├── .gitignore ├── README.md ├── python ├── __init__.py └── editor_utils │ ├── __init__.py │ ├── asset_utils.py │ ├── import_utils.py │ ├── skeletal_utils.py │ └── tag_utils.py └── stub ├── README.md ├── resources └── images │ └── unreal-stub-demo.gif └── unreal └── __init__.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Unreal Generated Files 2 | 3 | */Build 4 | */Binaries 5 | */DerivedDataCache 6 | */Intermediate 7 | */Saved 8 | 9 | # visual studio files 10 | */.vs 11 | */x64 12 | 13 | # personal files 14 | *LectureNotes 15 | 16 | # large files 17 | BuildingEscape/Content/StarterContent/Textures 18 | BuildingEscape/Content/StarterContent/HDRI 19 | 20 | # files 21 | *.VC.db 22 | *.VC.VC.opendb 23 | *.pch 24 | *_BuiltData.uasset 25 | 26 | *.suo 27 | *.pyc 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Unreal Python Dev 2 | ================================ 3 | 4 | This repository contains some simple Python utility scripts for UE5, and the `unreal` stub file with directions on how to set it up for auto-completion. I'm hoping it'll grow over time and be a useful resource for developers. 5 | 6 | --- 7 | 8 | ## Contents 9 | - Editor Utility Python Libraries 10 | - Unreal Engine 5 Python Stub file and how to set it up for auto-completion 11 | 12 | 13 | ## Resources 14 | - [Getting Started with Python in Unreal](https://youtu.be/0guOMTiwmhk) 15 | - [Unreal Engine 5 Python API Docs](https://docs.unrealengine.com/5.0/en-US/PythonAPI) 16 | - [Unreal Engine 5 Early Access](https://www.unrealengine.com/en-US/unreal-engine-5) 17 | - [Unreal Engine 5 Stub File and Setup](https://github.com/abcarlisle/unreal-python-dev/tree/master/stub) 18 | -------------------------------------------------------------------------------- /python/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright Epic Games, Inc. All Rights Reserved. 2 | -------------------------------------------------------------------------------- /python/editor_utils/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright Epic Games, Inc. All Rights Reserved. 2 | -------------------------------------------------------------------------------- /python/editor_utils/asset_utils.py: -------------------------------------------------------------------------------- 1 | # Copyright Epic Games, Inc. All Rights Reserved. 2 | 3 | import os 4 | import unreal 5 | 6 | asset_tools = unreal.AssetToolsHelpers.get_asset_tools() 7 | 8 | 9 | def rename_directory(source_directory_path, target_directory_path): 10 | """ 11 | Utility method for renaming a directory; equivalent to a 'move' operation. 12 | 13 | .. NOTE:: rename_directory from EditorAssetLibrary seems to be broken in 5.1: 14 | ref: https://forums.unrealengine.com/t/python-api-rename-directory-function-no-longer-works-in-5-1/748286 15 | 16 | .. USE:: rename_directory("/game/old_location", "/game/new_location") 17 | 18 | :param str source_directory_path: Game path to rename. 19 | :param str target_directory_path: Game path to rename the directory to. 20 | """ 21 | # discover all contents in the source directory 22 | source_directory_contents = unreal.EditorAssetLibrary.list_assets( 23 | source_directory_path, 24 | include_folder=True 25 | ) 26 | 27 | # find all assets and directories 28 | source_directory_assets = [c for c in source_directory_contents if "." in c] 29 | source_directory_folders = list(set(source_directory_contents) - set(source_directory_assets)) 30 | 31 | # create the new directory hierarchy 32 | unreal.EditorAssetLibrary.make_directory(target_directory_path) 33 | for folder in source_directory_folders: 34 | new_folder_name = folder.replace(source_directory_path, target_directory_path) 35 | unreal.EditorAssetLibrary.make_directory(new_folder_name) 36 | 37 | # rename the assets to the target directory path 38 | for asset_name in source_directory_assets: 39 | new_asset_name = asset_name.replace(source_directory_path, target_directory_path) 40 | unreal.EditorAssetLibrary.rename_asset(asset_name, new_asset_name) 41 | 42 | # delete old directory if empty 43 | if not unreal.EditorAssetLibrary.does_directory_have_assets(source_directory_path): 44 | unreal.EditorAssetLibrary.delete_directory(source_directory_path) 45 | else: 46 | unreal.log_warning( 47 | f"Could not delete '{source_directory_path}', " 48 | f"directory contains assets!" 49 | ) 50 | 51 | 52 | def get_asset_path(asset): 53 | """ 54 | Gets the unreal Object path if not a string. 55 | 56 | :param str or object asset: The asset to get the path for. 57 | :return: Returns the path to the given asset. 58 | :rtype str 59 | """ 60 | if isinstance(asset, unreal.Object): 61 | return asset.get_path_name() 62 | return asset 63 | 64 | 65 | def get_asset_paths(assets): 66 | """ 67 | Convenience method for getting a uniform list of asset paths. 68 | 69 | :param list(str) assets: The list of assets to get paths for. 70 | :return: Returns the list of asset paths. 71 | :rtype: list 72 | """ 73 | return [get_asset_path(asset) for asset in assets] 74 | 75 | 76 | def select_assets(assets): 77 | """ 78 | Convenience method for selecting a list of assets in the editor. 79 | This method will also move you to the location of the selected assets 80 | in the editor content browser in engine. 81 | 82 | :param str or list(object or str) assets: Asset list to select. 83 | """ 84 | # allow for str or list 85 | assets = assets if isinstance(assets, list) else [assets] 86 | 87 | # get a uniform list of paths 88 | assets_to_select = get_asset_paths(assets) 89 | unreal.EditorAssetLibrary.sync_browser_to_objects(assets_to_select) 90 | 91 | 92 | def get_selected_assets(as_paths=False): 93 | """ 94 | Get the selected assets in the editor content browser. 95 | 96 | :param bool as_paths: If True, returns the selected asset's paths. 97 | :return: Returns the selected assets in the editor content browser. 98 | :rtype: list(object or str) 99 | """ 100 | utility_base = unreal.GlobalEditorUtilityBase.get_default_object() 101 | selected_assets = list(utility_base.get_selected_assets()) 102 | 103 | # otherwise, return a list of the selected asset objects 104 | return get_asset_paths(selected_assets) if as_paths else selected_assets 105 | 106 | 107 | def save_asset(asset, force=False): 108 | """ 109 | Saves the given asset. 110 | 111 | :param str or object asset: The asset to save. 112 | :param bool force: Overrides the 'only_if_is_dirty' option. 113 | :return: Returns True if the asset saved successfully, False otherwise. 114 | :rtype: bool 115 | """ 116 | return unreal.EditorAssetLibrary.save_asset(get_asset_path(asset), not force) 117 | 118 | 119 | def save_assets(assets, force=False): 120 | """ 121 | Saves the the list of assets. 122 | 123 | :param list(str) assets: List of asset to save 124 | :param bool force: Overrides the 'only_if_is_dirty' option. 125 | :return: Returns a tuple containing the assets that did and did not save. 126 | :rtype: tuple(list(object), list(object)) 127 | """ 128 | return list(map(lambda asset: save_asset(asset, force=force), assets)) 129 | 130 | 131 | def is_valid_asset_path(asset_path): 132 | """ 133 | Simple method for validating if the given asset path is valid. 134 | 135 | :param str asset_path: Path to validate in engine. 136 | :return: Returns whether or not the path is valid. 137 | :rtype: bool 138 | """ 139 | return unreal.EditorAssetLibrary.does_asset_exist(asset_path) 140 | 141 | 142 | def duplicate_asset(source_asset, asset_name, asset_path, prompt=False): 143 | """ 144 | Convenience method for duplicating a target asset. 145 | 146 | :param object source_asset: Asset to duplicate. 147 | :param str asset_name: What to name the duplicate asset. 148 | :param str asset_path: Where to duplicate the asset to. 149 | :param bool prompt: Whether to prompt the show the dialog or not. 150 | :return: Returns the duplicated blueprint object. 151 | :rtype: object 152 | """ 153 | if not prompt: 154 | # determine the source and destination paths 155 | source_path = get_asset_path(source_asset) 156 | destination_path = os.path.join(asset_path, asset_name).replace("\\", "/") 157 | 158 | # duplicate the asset using the EditorAssetLibrary 159 | return unreal.EditorAssetLibrary.duplicate_asset(source_path, destination_path) 160 | 161 | # if prompt is True, use asset tools to prompt with a dialog 162 | return asset_tools.duplicate_asset_with_dialog(asset_name, asset_path, source_asset) 163 | -------------------------------------------------------------------------------- /python/editor_utils/import_utils.py: -------------------------------------------------------------------------------- 1 | # Copyright Epic Games, Inc. All Rights Reserved. 2 | 3 | import unreal 4 | from .asset_utils import asset_tools 5 | 6 | 7 | class ImportOptions(object): 8 | """Convenient enum class handler for normal import options.""" 9 | COMPUTE = unreal.FBXNormalImportMethod.FBXNIM_COMPUTE_NORMALS 10 | IMPORT = unreal.FBXNormalImportMethod.FBXNIM_IMPORT_NORMALS 11 | IMPORT_WITH_TANGENTS = ( 12 | unreal.FBXNormalImportMethod.FBXNIM_IMPORT_NORMALS_AND_TANGENTS 13 | ) 14 | 15 | 16 | class ImportTypes(object): 17 | """Convenient enum class handler for import type options.""" 18 | ANIMATION = unreal.FBXImportType.FBXIT_ANIMATION 19 | SKELETAL_MESH = unreal.FBXImportType.FBXIT_SKELETAL_MESH 20 | STATIC_MESH = unreal.FBXImportType.FBXIT_STATIC_MESH 21 | 22 | 23 | def create_fbx_import_task( 24 | fbx_path, 25 | game_path, 26 | asset_name, 27 | import_options, 28 | suppress_ui=True 29 | ): 30 | """ 31 | Creates a import task for the given fbx. 32 | 33 | :param str fbx_path: Path to import. 34 | :param str game_path: Path to import to. 35 | :param str asset_name: Name of the asset. 36 | :param FbxImportUI import_options: The import options for the import task. 37 | :param bool suppress_ui: Suppress the ui or not. 38 | :return: Returns the import task. 39 | :rtype: AssetImportTask. 40 | """ 41 | # create an import task 42 | import_task = unreal.AssetImportTask() 43 | 44 | # set the base properties on the import task 45 | import_task.filename = fbx_path 46 | import_task.destination_path = game_path 47 | import_task.destination_name = asset_name 48 | import_task.automated = suppress_ui 49 | 50 | # set the import options on the import task 51 | import_task.options = import_options 52 | return import_task 53 | 54 | 55 | def run_import_tasks(import_tasks): 56 | """ 57 | Imports the given list of tasks. 58 | 59 | :param list(FbxImportUI) import_tasks: The list of tasks to import. 60 | :return: Returns the assets that were imported. 61 | :rtype: list 62 | """ 63 | # import the given import tasks 64 | asset_tools.import_asset_tasks(import_tasks) 65 | 66 | # return the imported assets as paths 67 | return import_task.get_editor_property("imported_object_paths") 68 | 69 | 70 | def get_basic_skeletal_import_options( 71 | import_method=None, 72 | import_materials=False, 73 | import_textures=False 74 | ): 75 | """ 76 | Get some basic default import options for a SkeletalMesh import. 77 | 78 | :param int or enum import_method: The normals import method. 79 | :param bool import_materials: Whether to import the materials or not. 80 | :param bool import_textures: Whether to import the textures or not. 81 | :return: Returns the import options. 82 | :rtype: unreal.FbxImportUI 83 | """ 84 | # create the options handle 85 | options = unreal.FbxImportUI() 86 | 87 | # set to import as skeletal 88 | options.import_as_skeletal = True 89 | options.mesh_type_to_import = ImportTypes.SKELETAL_MESH 90 | 91 | # determine and set the import option for normals 92 | import_method = import_method or ImportOptions.COMPUTE 93 | options.skeletal_mesh_import_data.import_method = import_method 94 | 95 | # determine and set whether to import materials and textures 96 | options.import_materials = import_materials or False 97 | options.import_textures = import_textures or False 98 | return options 99 | -------------------------------------------------------------------------------- /python/editor_utils/skeletal_utils.py: -------------------------------------------------------------------------------- 1 | # Copyright Epic Games, Inc. All Rights Reserved. 2 | 3 | import unreal 4 | 5 | skeletal_mesh_library = unreal.EditorSkeletalMeshLibrary() 6 | 7 | 8 | def get_sockets(skeletal_mesh): 9 | """ 10 | Gets the sockets attached to the given SkeletalMesh. 11 | 12 | :param SkeletalMesh skeletal_mesh: Asset to get socket from. 13 | :param SkeletalMesh skeletal_mesh: Asset to get the skeleton from. 14 | :return: Returns the sockets for the given SkeletalMesh. 15 | :rtype: list or dict 16 | """ 17 | socket_count = skeletal_mesh.num_sockets() 18 | if not socket_count: 19 | return [] 20 | 21 | # get the sockets by index and return the socket objects 22 | return [skeletal_mesh.get_socket_by_index(s) for s in range(socket_count)] 23 | 24 | 25 | def get_socket_transform(socket): 26 | """ 27 | Convenience method for getting a sockets transform. 28 | 29 | :param Socket socket: The socket to get the transform for. 30 | :return: Returns the sockets transform as an unreal Transform object. 31 | :rtype: Transform 32 | """ 33 | return unreal.Transform( 34 | socket.relative_location, 35 | socket.relative_rotation, 36 | socket.relative_scale 37 | ) 38 | 39 | 40 | def create_socket(skeletal_mesh, parent_bone_name, transform, socket_name): 41 | """ 42 | Creates a socket on the given SkeletalMesh. 43 | 44 | :param SkeletalMesh skeletal_mesh: SkeletalMesh to creates socket on. 45 | :param str parent_bone_name: Name of the bone to create the socket on. 46 | :param Transform transform: The transform object to position the socket. 47 | :param str socket_name: The name of the socket. 48 | :return: Returns the created socket. 49 | :rtype: SkeletalMeshSocket 50 | """ 51 | skeleton = get_skeleton(skeletal_mesh) 52 | 53 | # FIXME: Currently not exposed in UE5 but is exposed for StaticMesh. 54 | # I believe in C++ you add a socket to a SkeletalMesh via FEditableSkeleton 55 | # which is also not exposed. 56 | skeleton.create_socket(parent_bone_name, transform, socket_name) 57 | return skeleton.find_socket(socket_name) 58 | 59 | 60 | def copy_socket_to_target(socket, skeletal_mesh): 61 | """ 62 | Copies the given socket to the given SkeletalMesh. 63 | 64 | :param SkeletalMeshSocket socket: Socket object to copy. 65 | :param SkeletalMesh skeletal_mesh: SkeletalMesh to copy the socket to. 66 | :return: Returns the copied socket object. 67 | :rtype: SkeletalMeshSocket 68 | """ 69 | 70 | # grab the socket name and parent bone 71 | socket_name = socket.socket_name 72 | bone_name = socket.bone_name 73 | 74 | # early return if the socket already exists 75 | if skeletal_mesh.find_socket(socket_name): 76 | return 77 | 78 | # determine the sockets transform location 79 | transform = get_socket_transform(socket) 80 | 81 | # create the socket on the target skeleton 82 | return create_socket(skeletal_mesh, bone_name, transform, socket_name) 83 | 84 | 85 | def copy_sockets_to_target(source_skeletal_mesh, target_skeletal_mesh): 86 | """ 87 | Copies the sockets from one SkeletalMesh to another. 88 | 89 | :param SkeletalMesh source_skeletal_mesh: Copy from SkeletalMesh. 90 | :param SkeletalMesh target_skeletal_mesh: Copy to SkeletalMesh. 91 | :return: Returns the sockets copied on to the target SkeletalMesh. 92 | :rtype: list(SkeletalMeshSocket) 93 | """ 94 | 95 | # early return if no sockets are found on the source asset 96 | if not source_skeletal_mesh.num_sockets(): 97 | asset_name = source_skeletal_mesh.get_full_name() 98 | unreal.log_warning( 99 | "No sockets could be found for '{asset_name}'!".format( 100 | asset_name=asset_name 101 | ) 102 | ) 103 | return 104 | 105 | # get the sockets from the source SkeletalMesh 106 | source_sockets = get_sockets(source_skeletal_mesh) 107 | 108 | # copy the sockets to the target skeleton 109 | [copy_socket_to_target(s, target_skeletal_mesh) for s in source_sockets] 110 | 111 | # return the new sockets on the target SkeletalMesh 112 | return get_sockets(target_skeletal_mesh) 113 | 114 | 115 | def get_skeleton(skeletal_mesh): 116 | """ 117 | Gets the skeleton from the given SkeletalMesh. 118 | 119 | :param SkeletalMesh skeletal_mesh: SkeletalMesh to get skeleton from. 120 | :return: Returns the skeleton associated with the given SkeletalMesh. 121 | :rtype: Skeleton 122 | """ 123 | # NOTE: Some releases don't have the skeleton property exposed on the asset 124 | # it can only be grabbed by using the get_editor_property method, which 125 | # works in all releases. 126 | return skeletal_mesh.get_editor_property("skeleton") 127 | 128 | 129 | def regenerate_lods(skeletal_mesh, number_of_lods=4): 130 | """ 131 | Regenerates the LODs for the given SkeletalMesh. 132 | 133 | :param SkeletalMesh skeletal_mesh: The asset to regenerate the LODs for. 134 | :param int number_of_lods: The number of LODs to regenerate. Default is 4. 135 | :return: Returns True if the LODs were regenerated, otherwise False. 136 | :rtype: bool 137 | """ 138 | return skeletal_mesh.regenerate_lod(number_of_lods) 139 | 140 | 141 | def get_vertex_count(skeletal_mesh, lod_level=0): 142 | """ 143 | Queries a skeletal mesh for the number of vertices present 144 | 145 | :param SkeletalMesh skeletal_mesh: SkeletalMesh to get the vertex count. 146 | :param int lod_level: The LOD to query. 147 | :return: Returns the vertex count for the given SkeletalMesh. 148 | :rtype: int 149 | """ 150 | # get the vertex count 151 | vertex_count = skeletal_mesh_library.get_num_verts( 152 | skeletal_mesh, 153 | lod_level 154 | ) 155 | # return 0 if the vertex count is None 156 | return vertex_count or 0 157 | -------------------------------------------------------------------------------- /python/editor_utils/tag_utils.py: -------------------------------------------------------------------------------- 1 | # Copyright Epic Games, Inc. All Rights Reserved. 2 | 3 | import unreal 4 | from . import asset_utils 5 | 6 | 7 | def set_tag_on_asset(asset, tag_name, tag_value, save=False): 8 | """ 9 | Sets the given metadata tag on the given asset. 10 | 11 | :param object asset: The asset to set metadata tags on. 12 | :param str tag_name: The name of the tag to set. 13 | :param str tag_value: The value of the tag to set. 14 | :param bool save: Save the asset or not. 15 | """ 16 | unreal.EditorAssetLibrary.set_metadata_tag(asset, tag_name, tag_value) 17 | 18 | # save the asset if specified 19 | if save: 20 | unreal.EditorAssetLibrary.save_loaded_asset(asset) 21 | 22 | 23 | def set_tags_on_asset(asset, tags, save=False): 24 | """ 25 | Sets tags on an asset given a dictionary with tag name, tag value pairs. 26 | 27 | :param object asset: The asset to set metadata tags on. 28 | :param dict tags: A dictionary of key value pairs where key is the name of 29 | the tag to set the the value is the value of the tag. 30 | :param bool save: Whether to save the asset or not after the tags are set. 31 | """ 32 | # loop through the given tags and set them on the given asset 33 | for tag_name, tag_value in tags.items(): 34 | set_tag_on_asset(asset, tag_name, tag_value) 35 | 36 | # save the asset if specified 37 | if save: 38 | unreal.EditorAssetLibrary.save_loaded_asset(asset) 39 | 40 | 41 | def get_asset_tags(asset): 42 | """ 43 | Gets the metadata tags for the given asset. 44 | 45 | :param Object asset: Asset object to retrieve metadata from. 46 | :return: Returns the metadata tags set on the given asset. 47 | :rtype: dict 48 | """ 49 | return unreal.EditorAssetLibrary.get_tag_values(asset.get_full_name()) 50 | -------------------------------------------------------------------------------- /stub/README.md: -------------------------------------------------------------------------------- 1 | 2 | Unreal Stub File 3 | ================================ 4 | 5 | This package contains the Unreal Engine 5 python stub file with directions on how to set it up for auto-completion in [PyCharm](https://www.jetbrains.com/pycharm). 6 | 7 | #### DISCLAIMER 8 | The `unreal` stub file is dynamic and will get regenerated as developers expose things in Blueprints for their Unreal Projects. The stub file in this package was generated from the vanilla Unreal Engine 5 Early Access build and is meant for convenience. 9 | 10 | #### STUB SIZE 11 | Due to the size of the `unreal` stub file (about 15 MB) I've converted it into a 12 | [Python Package](https://docs.python.org/3/tutorial/modules.html#packages) 13 | so your IDE doesn't crash/hang when you add it to your project 14 | (just don't click/right-click or try to open the `unreal` directory or `__init__.py` file until you follow the setup steps below, or your IDE might crash). 15 | 16 | # Setup Steps 17 | To allow your IDE to open the file you'll need to adjust your content and intellisense file size limits. 18 | The following steps show how to do this in [PyCharm](https://www.jetbrains.com/pycharm), 19 | but it's the same principle for all IDE's. 20 | 1. In PyCharm, go to `Help > Edit Custom Properties...` to open the `idea.properties` file. 21 | 2. Add the following lines, save the file and then restart PyCharm. 22 | ``` 23 | idea.max.content.load.filesize=250000 24 | idea.max.intellisense.filesize=250000 25 | ``` 26 | - ###### *Feel free to change the `filesize` property value to your liking.* 27 | 2. Mark the `stub` directory as a `Sources Root` by right-clicking the `stub` directory and going to `Mark Directory as > Sources Root` 28 | (you can also do this in your project settings via settings). 29 | 30 | 3. Open a python file, `import unreal` and then do `unreal.` (with the dot) to test that the auto-completion is working (see below). 31 | --- 32 | 33 | ![Unreal Stub Demo GIF](resources/images/unreal-stub-demo.gif) 34 | -------------------------------------------------------------------------------- /stub/resources/images/unreal-stub-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaronbcarlisle/unreal-python-dev/0e150ec78e2c70e93511951e7d7031a9d53f5c34/stub/resources/images/unreal-stub-demo.gif --------------------------------------------------------------------------------