How can I change a Static Mesh's Collision Preset and Collision Responses using Python?

Hey, everyone. Been wrestling with an issue while trying to automate some tasks in the editor using Python. I have almost 900 static meshes I need to adjust the collision of and the idea of having to perform that task by hand scares me!

So, I turned to Python to try and automate this task. I read the documentation I could find here and in the API reference and managed to come up with this code:

import unreal

# Create a transaction
with unreal.ScopedEditorTransaction(“My Transaction Test”) as trans:
full_path = ‘/Game/firstGame/pythonTest’
static_meshes = unreal.EditorAssetLibrary.list_assets(full_path, True, False)
filtered_static_meshes = [ ]

for asset in static_meshes:
    asset = asset.rsplit('.', 1)[0]

    if unreal.EditorAssetLibrary.does_asset_exist(asset):

        asset_data = unreal.EditorAssetLibrary.find_asset_data(asset)

        if asset_data.get_asset().get_class() == unreal.StaticMesh.static_class():
            filtered_static_meshes.append(asset)

text_label = "Working on Static Meshes..."

with unreal.ScopedSlowTask(len(filtered_static_meshes), text_label) as slow_task:

    slow_task.make_dialog(True)

    for mesh_path in filtered_static_meshes:

        if slow_task.should_cancel():
            break

        slow_task.enter_progress_frame(1, "Processing Mesh: {}".format(mesh_path)) # Advance progress by one frame. You can also update the dialog text in this call, if you want.
        static_mesh = unreal.EditorAssetLibrary.load_asset(mesh_path)

        if static_mesh:            
            body_setup = static_mesh.get_editor_property('body_setup')

            if body_setup:
                body_setup.set_editor_property('double_sided_geometry', True)
                default_body_instance = body_setup.get_editor_property('default_instance')

                if default_body_instance:
                    default_body_instance.set_editor_property('collision_profile_name', 'OverlapAll')
                    default_body_instance.set_editor_property('collision_enabled', unreal.CollisionEnabled.QUERY_ONLY)
                    default_body_instance.set_editor_property('object_type', unreal.CollisionChannel.ECC_WORLD_STATIC)
                    
                    collision_responses = default_body_instance.get_editor_property('collision_responses')
                    collision_array = collision_responses.get_editor_property('response_array')

                    for response_channel in collision_array:
                        response_channel.set_editor_property('response', unreal.CollisionResponseType.ECR_OVERLAP)

            unreal.EditorAssetLibrary.save_asset(mesh_path)

# Output the count
unreal.log('Number of Static Meshes set to use Double Sided Geometry and set to Collision Preset OverlapAll: {}'.format(len(filtered_static_meshes)))

I can reach the meshes, I can load them and even change some of their properties. But I’m really struggling with the Collision Preset/Collision Responses.

The code above will change the double sided geometry flag no problem, it will also change the Collision Preset name, the Collision Enabled dropdown and the Object Type. However, I noticed that this code can only change the Collision Preset name and that change has no effect on the dependent properties in the asset such as Collision Enabled, Object Type and the Collision Responses.

The Collision Enabled and Object Type I’m getting around by defining them by hand. But I can’t seem to get the Collision Responses to change.

I’m using Unreal 5.3, built from source. Let me go through an example to help illustrate my problem. Consider I have a Static Mesh with the following Collision attributes:

When I run the script, that asset will now have these Collision attributes:

Notice the double sided geometry flag is now true, the collision preset name has changed and that the collision enabled and object type have changed. These last two, only because I set them by hand. However, when I try to do the same with the Collision Responses, they never change.

The output of running the script is this:

Cmd: py prepFoliageAssets.py
LogSourceControl: Attempting ‘p4 fstat -Or C:/Users/ronis_1xgq/Perforce/p4_workspaces/yabab_RONI-PC_main_2554/firstGame/Content/firstGame/pythonTest/S_Acacia_semkO_0_Var10_lod1.uasset’
LogFileHelpers: InternalPromptForCheckoutAndSave started…
LogSavePackage: Moving output files for package: /Game/firstGame/pythonTest/S_Acacia_semkO_0_Var10_lod1
LogSavePackage: Moving ‘…/…/…/…/Perforce/p4_workspaces/yabab_RONI-PC_main_2554/firstGame/Saved/S_Acacia_semkO_0_Var10_lod13AD4A13E4532616AE80FC686D2E930CB.tmp’ to ‘…/…/…/…/Perforce/p4_workspaces/yabab_RONI-PC_main_2554/firstGame/Content/firstGame/pythonTest/S_Acacia_semkO_0_Var10_lod1.uasset’
LogFileHelpers: InternalPromptForCheckoutAndSave took 5 ms (total: 2.94 sec)
LogSourceControl: Attempting ‘p4 fstat -Or C:/Users/ronis_1xgq/Perforce/p4_workspaces/yabab_RONI-PC_main_2554/firstGame/Content/firstGame/pythonTest/S_Acacia_semkO_0_Var10_lod1.uasset’
LogPython: Number of Static Meshes set to use Double Sided Geometry and set to Collision Preset OverlapAll: 1
AssetCheck: New page: Asset Save: S_Acacia_semkO_0_Var10_lod1
LogContentValidation: Display: Validating /Script/Engine.StaticMesh /Game/firstGame/pythonTest/S_Acacia_semkO_0_Var10_lod1.S_Acacia_semkO_0_Var10_lod1

I noticed that the response_array in unreal.CollisionResponse only lists Collision Responses that have previously been set to a non-default value. Which, I guess is what they mean in unreal.BodyInstance when they mention " [Read-Write] Custom Channels for Responses". For example, consider the same Static Mesh from before is now set to Collision Preset BlockAll before the script is run, but now we actually log the channels in collision array with the following change to the script:

import unreal

# Create a transaction
with unreal.ScopedEditorTransaction(“My Transaction Test”) as trans:
full_path = ‘/Game/firstGame/pythonTest’
static_meshes = unreal.EditorAssetLibrary.list_assets(full_path, True, False)
filtered_static_meshes = [ ]

for asset in static_meshes:
    asset = asset.rsplit('.', 1)[0]

    if unreal.EditorAssetLibrary.does_asset_exist(asset):

        asset_data = unreal.EditorAssetLibrary.find_asset_data(asset)

        if asset_data.get_asset().get_class() == unreal.StaticMesh.static_class():
            filtered_static_meshes.append(asset)

text_label = "Working on Static Meshes..."

with unreal.ScopedSlowTask(len(filtered_static_meshes), text_label) as slow_task:

    slow_task.make_dialog(True)

    for mesh_path in filtered_static_meshes:

        if slow_task.should_cancel():
            break

        slow_task.enter_progress_frame(1, "Processing Mesh: {}".format(mesh_path)) # Advance progress by one frame. You can also update the dialog text in this call, if you want.
        static_mesh = unreal.EditorAssetLibrary.load_asset(mesh_path)

        if static_mesh:            
            body_setup = static_mesh.get_editor_property('body_setup')

            if body_setup:
                body_setup.set_editor_property('double_sided_geometry', True)
                default_body_instance = body_setup.get_editor_property('default_instance')

                if default_body_instance:
                    default_body_instance.set_editor_property('collision_profile_name', 'OverlapAll')
                    default_body_instance.set_editor_property('collision_enabled', unreal.CollisionEnabled.QUERY_ONLY)
                    default_body_instance.set_editor_property('object_type', unreal.CollisionChannel.ECC_WORLD_STATIC)
                    
                    collision_responses = default_body_instance.get_editor_property('collision_responses')
                    collision_array = collision_responses.get_editor_property('response_array')

                    for response_channel in collision_array:
                        print(response_channel.get_editor_property('channel'))
                        response_channel.set_editor_property('response', unreal.CollisionResponseType.ECR_OVERLAP)

            unreal.EditorAssetLibrary.save_asset(mesh_path)

# Output the count
unreal.log('Number of Static Meshes set to use Double Sided Geometry and set to Collision Preset OverlapAll: {}'.format(len(filtered_static_meshes)))

If the Static Mesh is set like this:

The output of the script will be:

Cmd: py prepFoliageAssets.py
LogPython: Lyra_TraceChannel_Interaction
LogPython: Lyra_TraceChannel_Weapon
LogPython: Lyra_TraceChannel_Weapon_Capsule
LogPython: Lyra_TraceChannel_Weapon_Multi
LogSourceControl: Attempting ‘p4 fstat -Or C:/Users/ronis_1xgq/Perforce/p4_workspaces/yabab_RONI-PC_main_2554/firstGame/Content/firstGame/pythonTest/S_Acacia_semkO_0_Var10_lod1.uasset’
LogFileHelpers: InternalPromptForCheckoutAndSave started…
LogSavePackage: Moving output files for package: /Game/firstGame/pythonTest/S_Acacia_semkO_0_Var10_lod1
LogSavePackage: Moving ‘…/…/…/…/Perforce/p4_workspaces/yabab_RONI-PC_main_2554/firstGame/Saved/S_Acacia_semkO_0_Var10_lod186E07B5E41277AB15755958F55AE147A.tmp’ to ‘…/…/…/…/Perforce/p4_workspaces/yabab_RONI-PC_main_2554/firstGame/Content/firstGame/pythonTest/S_Acacia_semkO_0_Var10_lod1.uasset’
LogFileHelpers: InternalPromptForCheckoutAndSave took 5 ms (total: 3.25 sec)
LogSourceControl: Attempting ‘p4 fstat -Or C:/Users/ronis_1xgq/Perforce/p4_workspaces/yabab_RONI-PC_main_2554/firstGame/Content/firstGame/pythonTest/S_Acacia_semkO_0_Var10_lod1.uasset’
LogPython: Number of Static Meshes set to use Double Sided Geometry and set to Collision Preset OverlapAll: 1
AssetCheck: New page: Asset Save: S_Acacia_semkO_0_Var10_lod1
LogContentValidation: Display: Validating /Script/Engine.StaticMesh /Game/firstGame/pythonTest/S_Acacia_semkO_0_Var10_lod1.S_Acacia_semkO_0_Var10_lod1

However, if I set the Collision Preset to “Custom…” and manually set the Collision Responses to ‘Overlap’.

Then when I run the same script, the output is:

Cmd: py prepFoliageAssets.py
LogPython: Lyra_TraceChannel_Interaction
LogPython: Lyra_TraceChannel_Weapon
LogPython: Lyra_TraceChannel_Weapon_Capsule
LogPython: Lyra_TraceChannel_Weapon_Multi
LogPython: WorldStatic
LogPython: WorldDynamic
LogPython: Pawn
LogPython: Visibility
LogPython: Camera
LogPython: PhysicsBody
LogPython: Vehicle
LogPython: Destructible
LogPython: Lyra_TraceChannel_AimAssist
LogSourceControl: Attempting ‘p4 fstat -Or C:/Users/ronis_1xgq/Perforce/p4_workspaces/yabab_RONI-PC_main_2554/firstGame/Content/firstGame/pythonTest/S_Acacia_semkO_0_Var10_lod1.uasset’
LogFileHelpers: InternalPromptForCheckoutAndSave started…
LogSavePackage: Moving output files for package: /Game/firstGame/pythonTest/S_Acacia_semkO_0_Var10_lod1
LogSavePackage: Moving ‘…/…/…/…/Perforce/p4_workspaces/yabab_RONI-PC_main_2554/firstGame/Saved/S_Acacia_semkO_0_Var10_lod16B0387B740BA71FC05D92CBE50B5FEEA.tmp’ to ‘…/…/…/…/Perforce/p4_workspaces/yabab_RONI-PC_main_2554/firstGame/Content/firstGame/pythonTest/S_Acacia_semkO_0_Var10_lod1.uasset’
LogFileHelpers: InternalPromptForCheckoutAndSave took 5 ms (total: 3.51 sec)
LogSourceControl: Attempting ‘p4 fstat -Or C:/Users/ronis_1xgq/Perforce/p4_workspaces/yabab_RONI-PC_main_2554/firstGame/Content/firstGame/pythonTest/S_Acacia_semkO_0_Var10_lod1.uasset’
LogPython: Number of Static Meshes set to use Double Sided Geometry and set to Collision Preset OverlapAll: 1
AssetCheck: New page: Asset Save: S_Acacia_semkO_0_Var10_lod1
LogContentValidation: Display: Validating /Script/Engine.StaticMesh /Game/firstGame/pythonTest/S_Acacia_semkO_0_Var10_lod1.S_Acacia_semkO_0_Var10_lod1

Notice that now all Collision Responses are in the array.

A seemingly simple task has been taking me days to figure out. Has anyone ever done this? Is it even possible or am I banging my head against an API that doesn’t support what I’m trying to do?

Any help is appreciated!
Thanks!