| name | level-actors |
| display_name | Level Actors & Editor Subsystems |
| description | List, find, place, move, rotate, and modify actors in the current level (ActorService + editor subsystems). Use when the user asks to find/list actors in the level, move or rotate an actor, set an actor's properties/folder/transform, or spawn/delete level actors. |
| vibeue_classes | ["ActorService"] |
| unreal_classes | ["EditorActorSubsystem","LevelEditorSubsystem"] |
🧠 Brains complement: IF an unreal-engine-skills-manager tool (external MCP) exists in this session, call it with {action: "load", skill: "actors-and-components"} for UE domain knowledge on this topic — correct APIs, architecture, best practices — and treat it as the rubric for any review / "best practices" question. If no such tool is available (e.g. running under Claude Code or Codex without that MCP), skip this line entirely and proceed with this skill alone — do NOT attempt the call.
Level Actors Skill
🔀 Engine owns the basics now. In the Unreal 5.8 consolidation, listing / finding / spawning /
moving / transforming / selecting actors moved to Unreal's native toolsets — ActorTools,
SceneTools, and EditorAppToolset (selection, camera, PIE, screenshots), reachable via
call_tool. Equivalently you can drive unreal.EditorActorSubsystem /
unreal.LevelEditorSubsystem directly from execute_python_code (shown throughout below).
VibeUE's ActorService was trimmed to only the delta the engine doesn't provide: transform
lock (set_actor_lock_location), absolute-transform flags, preserve-scale-ratio, viewport camera
framing (get_actor_view_camera / calculate_actor_view / set_viewport_camera), and full
property dumps (get_all_properties). ActorService.add_actor / set_property / get_property
no longer exist — use the engine toolsets or EditorActorSubsystem for spawning and property edits.
Critical Rules
� Creating a "Basic" Level Requires new_level_from_template, NOT new_level
When the user asks to create a new level (especially "Basic", "Default", or with a sky/floor), always use new_level_from_template:
subsys = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem)
subsys.new_level("/Game/Maps/MyLevel")
subsys.new_level_from_template("/Game/Maps/MyLevel", "/Engine/Maps/Templates/Template_Default")
Template names: Template_Default = Basic, OpenWorld, TimeOfDay_Default, VR-Basic
�🚫 DEPRECATED: unreal.EditorLevelLibrary
DO NOT use unreal.EditorLevelLibrary. The entire Editor Scripting Utilities Plugin is deprecated in UE 5.7+. Use unreal.EditorActorSubsystem via unreal.get_editor_subsystem() for all level actor operations.
all_actors = unreal.EditorLevelLibrary.get_all_level_actors()
unreal.EditorLevelLibrary.spawn_actor_from_class(...)
actor_subsys = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
all_actors = actor_subsys.get_all_level_actors()
actor_subsys.spawn_actor_from_class(...)
🚫 get_all_level_actors_of_class DOES NOT EXIST
EditorActorSubsystem has NO get_all_level_actors_of_class() method. Always use get_all_level_actors() + isinstance() filtering:
actor_subsys.get_all_level_actors_of_class(unreal.Landscape)
landscapes = [a for a in actor_subsys.get_all_level_actors() if isinstance(a, unreal.Landscape)]
lights = [a for a in actor_subsys.get_all_level_actors() if isinstance(a, unreal.PointLight)]
🚨 Verified AttributeError traps (UE 5.7 Python) — don't burn iterations rediscovering these
| You might write | ❌ Why it fails | ✅ Use instead |
|---|
comp.get_static_mesh() | StaticMeshComponent has no get_static_mesh | comp.get_editor_property("static_mesh") (read) / comp.set_static_mesh(mesh) (write) |
actor.get_components() | Actors have no get_components() | actor.get_components_by_class(unreal.StaticMeshComponent) → array |
dir_light.directional_light_component | no such attribute | dir_light.light_component (ALL unreal.Light actors use light_component) |
comp.set_editor_property("cast_shadow", …) | property is plural | comp.set_editor_property("cast_shadows", False) |
unreal.HorizontalTextAligment | UE misspells it | unreal.HorizTextAligment / unreal.VerticalTextAligment (for TextRenderActor) |
When unsure of a property/method name, call discover_python_class(class_name="unreal.X") ONCE up front
rather than guessing repeatedly — its doc_string lists the real editor-property names.
Migration guide:
Deprecated (EditorLevelLibrary) | Replacement (EditorActorSubsystem) |
|---|
get_all_level_actors() | actor_subsys.get_all_level_actors() |
get_all_level_actors_of_class(cls) | [a for a in actor_subsys.get_all_level_actors() if isinstance(a, cls)] |
spawn_actor_from_class() | actor_subsys.spawn_actor_from_class() |
destroy_actor() | actor_subsys.destroy_actor() |
get_selected_level_actors() | actor_subsys.get_selected_level_actors() |
set_actor_selection_state() | actor_subsys.set_actor_selection_state() |
🚨 Spawn Static Mesh Actors via spawn_actor_from_class + comp.set_static_mesh()
Do not spawn a bare actor and then poke StaticMesh through a generic property setter — that
historically produced actors with zero-extent bounds (invisible in the viewport even though the
mesh reads back as set). (The old ActorService.add_actor / set_property helpers that caused this
were removed in 5.8.)
ALWAYS use spawn_actor_from_class + comp.set_static_mesh():
import unreal
actor_subsys = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
mesh = unreal.load_asset("/Engine/BasicShapes/Cube")
material = unreal.load_asset("/Game/Materials/M_MyMat")
actor = actor_subsys.spawn_actor_from_class(
unreal.StaticMeshActor,
unreal.Vector(0, 0, 100),
unreal.Rotator(0, 0, 0)
)
actor.set_actor_label("MyMeshActor")
actor.set_actor_scale3d(unreal.Vector(2, 2, 2))
comp = actor.static_mesh_component
comp.set_static_mesh(mesh)
comp.set_material(0, material)
bounds = actor.get_actor_bounds(False)
print(f"Bounds extent: {bounds[1].x:.0f}, {bounds[1].y:.0f}, {bounds[1].z:.0f}")
If bounds extent is 0, 0, 0 after spawning, the mesh was not applied correctly.
⚠️ Get Subsystem Instance First
import unreal
actor_subsys = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
level_subsys = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem)
⚠️ Spawning Actors - Use Class Object, NOT Path String
actor = subsys.spawn_actor_from_class(unreal.StaticMeshActor, location, rotation)
bp_class = unreal.EditorAssetLibrary.load_blueprint_class("/Game/BP_Enemy")
actor = subsys.spawn_actor_from_class(bp_class, location, rotation)
⚠️ FVector and FRotator Objects Required
location = unreal.Vector(100, 200, 0)
rotation = unreal.Rotator(pitch=-45)
location = "(X=100,Y=200,Z=0)"
🚨 Constructor Argument Order Traps: Rotator is (Roll, Pitch, Yaw), Color is BGRA
The Python constructors do NOT use the order you expect. Always use keyword arguments — never positional — for unreal.Rotator and unreal.Color:
unreal.Rotator(-45, 0, 0)
unreal.Rotator(pitch=-45)
unreal.Rotator(roll=0, pitch=-90, yaw=45)
unreal.Color(255, 180, 100)
unreal.Color(r=255, g=180, b=100, a=255)
unreal.LinearColor(1.0, 0.7, 0.4, 1.0)
Pitch sign convention: negative pitch = downward (pitch=-90 points straight down). For lights/cameras use negative pitch to aim at the ground.
⚠️ Refresh Viewport After Changes
level_subsys = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem)
level_subsys.editor_invalidate_viewports()
Workflows
Create Level from Template
🚨 Critical: new_level() creates a blank/empty level with NO content (no floor, sky, lights, or player start). To get the standard "Basic" level with default content, ALWAYS use new_level_from_template() with /Engine/Maps/Templates/Template_Default.
Available UE 5.7 templates in /Engine/Maps/Templates/:
| Template path | Description |
|---|
/Engine/Maps/Templates/Template_Default | Basic — floor, sky sphere, directional light, player start |
/Engine/Maps/Templates/OpenWorld | Open world with large terrain |
/Engine/Maps/Templates/TimeOfDay_Default | Time-of-day sky setup |
/Engine/Maps/Templates/VR-Basic | VR template |
Pattern: Create a new level from the Basic template
import unreal
subsys = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem)
save_path = "/Game/Maps/MyLevel"
template = "/Engine/Maps/Templates/Template_Default"
result = subsys.new_level_from_template(save_path, template)
print(f"Created: {result}")
⚠️ Cannot delete the currently-loaded level. If the target path already exists and is loaded, you must first switch away using new_level() to a temp path, then create from template:
import unreal
subsys = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem)
target = "/Game/Maps/MyLevel"
template = "/Engine/Maps/Templates/Template_Default"
subsys.new_level("/Game/Maps/__TempSwitch")
result = subsys.new_level_from_template(target, template)
print(f"Created from template: {result}")
Save the Current Level (Save / Save As)
Getting the editor world — it lives on UnrealEditorSubsystem, NOT LevelEditorSubsystem, NOT the deprecated EditorLevelLibrary:
import unreal
unreal.get_editor_subsystem(unreal.LevelEditorSubsystem).get_editor_world()
unreal.EditorLevelLibrary.get_editor_world()
world = unreal.get_editor_subsystem(unreal.UnrealEditorSubsystem).get_editor_world()
Which save call to use:
- Level already saved at a path →
unreal.get_editor_subsystem(unreal.LevelEditorSubsystem).save_current_level()
- Untitled/never-saved level, or "save as " →
EditorLoadingAndSavingUtils.save_map(world, asset_path) (both args required, in that order):
import unreal
world = unreal.get_editor_subsystem(unreal.UnrealEditorSubsystem).get_editor_world()
ok = unreal.EditorLoadingAndSavingUtils.save_map(world, "/Game/Maps/MyLevel")
print(f"Saved: {ok}")
To also make the saved level the editor startup map, load the project-settings skill and set maps / EditorStartupMap with the full asset path (/Game/Maps/MyLevel.MyLevel).
Spawn Built-in Actor
import unreal
subsys = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
actor = subsys.spawn_actor_from_class(
unreal.StaticMeshActor,
unreal.Vector(0, 0, 100),
unreal.Rotator(0, 0, 0)
)
level_subsys = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem)
level_subsys.editor_invalidate_viewports()
Spawn Blueprint Actor
import unreal
subsys = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
bp_class = unreal.EditorAssetLibrary.load_blueprint_class("/Game/Blueprints/BP_Enemy")
if bp_class:
actor = subsys.spawn_actor_from_class(bp_class, unreal.Vector(500, 0, 100), unreal.Rotator(0, 0, 0))
Get Level Actors
⚠️ unreal.EditorLevelLibrary is DEPRECATED. Always use EditorActorSubsystem instead.
import unreal
actor_subsys = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
all_actors = actor_subsys.get_all_level_actors()
lights = [a for a in all_actors if isinstance(a, unreal.PointLight)]
for actor in all_actors[:10]:
loc = actor.get_actor_location()
print(f"{actor.get_name()} at ({loc.x}, {loc.y}, {loc.z})")
Transform Operations
import unreal
loc = actor.get_actor_location()
actor.set_actor_location(unreal.Vector(100, 200, 300), False, False)
rot = actor.get_actor_rotation()
actor.set_actor_rotation(unreal.Rotator(pitch=-90, yaw=45), False)
actor.add_actor_local_rotation(unreal.Rotator(pitch=30), False, False)
scale = actor.get_actor_scale3d()
actor.set_actor_scale3d(unreal.Vector(2, 2, 2))
actor.set_actor_transform(
unreal.Transform(
location=unreal.Vector(500, 500, 200),
rotation=unreal.Rotator(pitch=45),
scale=unreal.Vector(1.5, 1.5, 1.5)),
False, False)
Sweep Moves & Collision Checks (HitResult)
Three verified gotchas (UE 5.7):
HitResult fields cannot be read as attributes — hit.blocking_hit / hit.bBlockingHit raise "property is protected". Use hit.to_tuple()[0] (the blocking-hit bool) or hit.export_text() (full readable dump: Location, ImpactPoint, Normal, Distance, Time).
set_actor_location(loc, sweep=True, ...) does NOT detect collisions in the editor world — it reports no hit even when moving straight through another actor. Don't rely on it for collision checks.
- The definitive checks: compare the actor's final position to the target, and/or trace the path explicitly with
SystemLibrary.line_trace_single (this DOES hit editor actors):
import unreal
actor.set_actor_location(target, True, False)
arrived = unreal.Vector.distance(actor.get_actor_location(), target) < 1.0
world = unreal.get_editor_subsystem(unreal.UnrealEditorSubsystem).get_editor_world()
hit = unreal.SystemLibrary.line_trace_single(
world, start_loc, target,
unreal.TraceTypeQuery.ECC_VISIBILITY,
False, [actor],
unreal.DrawDebugTrace.NONE, True)
if hit and hit.to_tuple()[0]:
blocker = hit.to_tuple()[9]
print(f"Blocked by {blocker.get_actor_label()}: {hit.export_text()[:200]}")
Organize: Rename, Folders, Attach/Detach, Tags
import unreal
actor.set_actor_label("MainLight")
actor.set_folder_path("TestLights")
actor.attach_to_actor(parent_actor, "",
unreal.AttachmentRule.KEEP_WORLD,
unreal.AttachmentRule.KEEP_WORLD,
unreal.AttachmentRule.KEEP_WORLD, False)
actor.detach_from_actor(
unreal.DetachmentRule.KEEP_WORLD,
unreal.DetachmentRule.KEEP_WORLD,
unreal.DetachmentRule.KEEP_WORLD)
actor.tags = ["Test", "Lighting"]
tagged = [a for a in actor_subsys.get_all_level_actors() if "Lighting" in a.tags]
Light Actors (PointLight, SpotLight, DirectionalLight)
All unreal.Light actors expose their component as actor.light_component — no get_component_by_class needed:
import unreal
comp = light_actor.light_component
comp.set_intensity(10000)
comp.set_light_color(unreal.LinearColor(1.0, 0.7, 0.4))
intensity = comp.get_editor_property("intensity")
Selection
import unreal
subsys = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
subsys.select_nothing()
subsys.set_actor_selection_state(actor, True)
selected = subsys.get_selected_level_actors()
Move Actor to Viewport Camera
import unreal
actor_subsys = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
editor_subsys = unreal.get_editor_subsystem(unreal.UnrealEditorSubsystem)
camera_info = editor_subsys.get_level_viewport_camera_info()
if camera_info:
cam_loc, cam_rot = camera_info
forward_vec = cam_rot.get_forward_vector()
new_loc = cam_loc + (forward_vec * 200.0)
actor.set_actor_location(new_loc, False, True)
Destroy/Duplicate Actors
import unreal
subsys = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
subsys.destroy_actor(actor)
dup = subsys.duplicate_actor(actor)
loc = dup.get_actor_location()
dup.set_actor_location(unreal.Vector(loc.x + 200, loc.y, loc.z), False, False)
Camera View Methods (for screenshots and verification)
Use ActorService to position the viewport camera to frame actors from specific directions. (For a
plain "focus on these actors" with no specific direction, the engine EditorAppToolset
FocusOnActors action via call_tool also works — ActorService adds the directional/padding
framing the engine toolset lacks.)
⚠️ NEVER guess camera coordinates with set_viewport_camera. Manual positions almost always point at sky or empty space. Always use get_actor_view_camera which auto-calculates position from the actor's bounding box.
import unreal
actor_service = unreal.ActorService
view = actor_service.get_actor_view_camera("MyLandscape", unreal.ViewDirection.TOP)
view = actor_service.get_actor_view_camera("MyBuilding", unreal.ViewDirection.FRONT, 1.5)
view = actor_service.calculate_actor_view("MyActor", unreal.ViewDirection.RIGHT, 1.2)
Directions: TOP, BOTTOM, LEFT, RIGHT, FRONT, BACK
Padding: 1.0=tight, 1.2=default, 2.0=far, 3.0=very far (use for large structures)
Choosing the Right View
| Goal | Use |
|---|
| Overview of a large structure | TOP with padding 2.0–3.0 |
| See the front face | FRONT with padding 1.5–2.5 |
| Full castle/building view | FRONT padding 3.0, or TOP padding 2.5 |
| Check layout from above | TOP padding 1.5 |
| Profile view | LEFT or RIGHT |
⚠️ Do NOT switch to set_viewport_camera with manual coordinates to get a "better angle". Manual positions almost always miss the subject and point at sky or empty space. If the view isn't wide enough, increase the padding or switch to TOP. There is no need for a diagonal camera — TOP and FRONT with adequate padding cover every screenshot use case.
Transform Locking & Constraints
🔒 Location Locking (Per-Actor, Native UE5)
UE5 has a native bLockLocation property on all actors. When locked, the actor cannot be moved via viewport gizmos (but CAN still be moved via code).
import unreal
unreal.ActorService.set_actor_lock_location("MyCube", True)
locked = unreal.ActorService.get_actor_lock_location("MyCube")
print(f"Locked: {locked}")
unreal.ActorService.set_actor_lock_location("MyCube", False)
🔒 Scale Ratio Lock (Uniform Scaling Padlock — Global Editor Setting)
The padlock icon next to Scale in the Details panel is the Preserve Scale Ratio setting. When enabled, scaling any axis scales ALL axes proportionally. This is a global editor preference, not per-actor.
import unreal
unreal.ActorService.set_preserve_scale_ratio(True)
unreal.ActorService.set_preserve_scale_ratio(False)
locked = unreal.ActorService.get_preserve_scale_ratio()
print(f"Scale ratio locked: {locked}")
⚠️ There is NO Per-Actor Rotation or Scale Lock
UE5 does NOT have a per-actor lock for rotation or scale. There is no bLockRotation or bLockScale property on actors. Only location locking (set_actor_lock_location) is per-actor.
If user asks to "lock rotation" or "lock scale on this actor specifically":
- Explain this limitation clearly
- For uniform scaling, use
set_preserve_scale_ratio(True) (global, affects all actors)
- For location locking, use
set_actor_lock_location
- For world-space independence, use absolute transform flags
Absolute Transform Flags (Per-Component)
Make location/rotation/scale world-space instead of relative to parent. Useful when attaching actors but needing independent positioning.
import unreal
unreal.ActorService.set_absolute_transform("MyCube", False, True, False)
loc, rot, scale = unreal.ActorService.get_absolute_transform("MyCube")
print(f"Absolute: loc={loc}, rot={rot}, scale={scale}")
Sample scripts (run via execute_python_code)
scripts/manipulate_actors.txt — list level actors, find by class, move/rotate by name.