Skip to main content
Glama
animation.ts25 kB
import { UnrealBridge } from '../unreal-bridge.js'; import { validateAssetParams } from '../utils/validation.js'; import { interpretStandardResult, coerceBoolean, coerceString, coerceStringArray } from '../utils/result-helpers.js'; type CreateAnimationBlueprintSuccess = { success: true; message: string; path: string; exists?: boolean; skeleton?: string; warnings?: string[]; details?: string[]; }; type CreateAnimationBlueprintFailure = { success: false; message: string; error: string; path?: string; exists?: boolean; skeleton?: string; warnings?: string[]; details?: string[]; }; type PlayAnimationSuccess = { success: true; message: string; warnings?: string[]; details?: string[]; actorName?: string; animationType?: string; assetPath?: string; }; type PlayAnimationFailure = { success: false; message: string; error: string; warnings?: string[]; details?: string[]; availableActors?: string[]; actorName?: string; animationType?: string; assetPath?: string; }; export class AnimationTools { constructor(private bridge: UnrealBridge) {} async createAnimationBlueprint(params: { name: string; skeletonPath: string; savePath?: string; }): Promise<CreateAnimationBlueprintSuccess | CreateAnimationBlueprintFailure> { try { const targetPath = params.savePath ?? '/Game/Animations'; const validation = validateAssetParams({ name: params.name, savePath: targetPath }); if (!validation.valid) { const message = validation.error ?? 'Invalid asset parameters'; return { success: false, message, error: message }; } const sanitized = validation.sanitized; const assetName = sanitized.name; const assetPath = sanitized.savePath ?? targetPath; const script = this.buildCreateAnimationBlueprintScript({ name: assetName, path: assetPath, skeletonPath: params.skeletonPath }); const response = await this.bridge.executePython(script); return this.parseAnimationBlueprintResponse(response, assetName, assetPath); } catch (err) { const error = `Failed to create Animation Blueprint: ${err}`; return { success: false, message: error, error: String(err) }; } } async addStateMachine(params: { blueprintPath: string; machineName: string; states: Array<{ name: string; animation?: string; isEntry?: boolean; isExit?: boolean; }>; transitions?: Array<{ sourceState: string; targetState: string; condition?: string; }>; }): Promise<{ success: true; message: string } | { success: false; error: string }> { try { if (!params.blueprintPath || !params.machineName) { return { success: false, error: 'blueprintPath and machineName are required' }; } const commands: string[] = [ `AddAnimStateMachine ${params.blueprintPath} ${params.machineName}` ]; for (const state of params.states) { const animationName = state.animation ?? ''; commands.push( `AddAnimState ${params.blueprintPath} ${params.machineName} ${state.name} ${animationName}` ); if (state.isEntry) { commands.push(`SetAnimStateEntry ${params.blueprintPath} ${params.machineName} ${state.name}`); } if (state.isExit) { commands.push(`SetAnimStateExit ${params.blueprintPath} ${params.machineName} ${state.name}`); } } if (params.transitions) { for (const transition of params.transitions) { commands.push( `AddAnimTransition ${params.blueprintPath} ${params.machineName} ${transition.sourceState} ${transition.targetState}` ); if (transition.condition) { commands.push( `SetAnimTransitionRule ${params.blueprintPath} ${params.machineName} ${transition.sourceState} ${transition.targetState} ${transition.condition}` ); } } } await this.bridge.executeConsoleCommands(commands); return { success: true, message: `State machine ${params.machineName} added to ${params.blueprintPath}` }; } catch (err) { return { success: false, error: `Failed to add state machine: ${err}` }; } } async createBlendSpace(params: { name: string; savePath?: string; dimensions?: 1 | 2; skeletonPath?: string; horizontalAxis?: { name: string; minValue: number; maxValue: number }; verticalAxis?: { name: string; minValue: number; maxValue: number }; samples?: Array<{ animation: string; x: number; y?: number }>; }): Promise<{ success: true; message: string; path: string } | { success: false; error: string }> { try { const targetPath = params.savePath ?? '/Game/Animations'; const validation = validateAssetParams({ name: params.name, savePath: targetPath }); if (!validation.valid) { return { success: false, error: validation.error ?? 'Invalid asset parameters' }; } const sanitized = validation.sanitized; const assetName = sanitized.name; const assetPath = sanitized.savePath ?? targetPath; const dimensions = params.dimensions === 2 ? 2 : 1; const blendSpaceType = dimensions === 2 ? 'BlendSpace' : 'BlendSpace1D'; const commands: string[] = [ `CreateAsset ${blendSpaceType} ${assetName} ${assetPath}`, `echo Creating ${blendSpaceType} ${assetName} at ${assetPath}` ]; if (params.skeletonPath) { commands.push(`SetBlendSpaceSkeleton ${assetName} ${params.skeletonPath}`); } if (params.horizontalAxis) { commands.push( `SetBlendSpaceAxis ${assetName} Horizontal ${params.horizontalAxis.name} ${params.horizontalAxis.minValue} ${params.horizontalAxis.maxValue}` ); } if (dimensions === 2 && params.verticalAxis) { commands.push( `SetBlendSpaceAxis ${assetName} Vertical ${params.verticalAxis.name} ${params.verticalAxis.minValue} ${params.verticalAxis.maxValue}` ); } if (params.samples) { for (const sample of params.samples) { const coords = dimensions === 1 ? `${sample.x}` : `${sample.x} ${sample.y ?? 0}`; commands.push(`AddBlendSpaceSample ${assetName} ${sample.animation} ${coords}`); } } await this.bridge.executeConsoleCommands(commands); return { success: true, message: `Blend Space ${assetName} created`, path: `${assetPath}/${assetName}` }; } catch (err) { return { success: false, error: `Failed to create blend space: ${err}` }; } } async setupControlRig(params: { name: string; skeletonPath: string; savePath?: string; controls?: Array<{ name: string; type: 'Transform' | 'Float' | 'Bool' | 'Vector'; bone?: string; defaultValue?: unknown; }>; }): Promise<{ success: true; message: string; path: string } | { success: false; error: string }> { try { const targetPath = params.savePath ?? '/Game/Animations'; const validation = validateAssetParams({ name: params.name, savePath: targetPath }); if (!validation.valid) { return { success: false, error: validation.error ?? 'Invalid asset parameters' }; } const sanitized = validation.sanitized; const assetName = sanitized.name; const assetPath = sanitized.savePath ?? targetPath; const fullPath = `${assetPath}/${assetName}`; const commands: string[] = [ `CreateAsset ControlRig ${assetName} ${assetPath}`, `SetControlRigSkeleton ${assetName} ${params.skeletonPath}` ]; if (params.controls) { for (const control of params.controls) { commands.push( `AddControlRigControl ${assetName} ${control.name} ${control.type} ${control.bone ?? ''}` ); if (control.defaultValue !== undefined) { commands.push( `SetControlRigDefault ${assetName} ${control.name} ${JSON.stringify(control.defaultValue)}` ); } } } await this.bridge.executeConsoleCommands(commands); return { success: true, message: `Control Rig ${assetName} created`, path: fullPath }; } catch (err) { return { success: false, error: `Failed to setup control rig: ${err}` }; } } async createLevelSequence(params: { name: string; savePath?: string; frameRate?: number; duration?: number; tracks?: Array<{ actorName: string; trackType: 'Transform' | 'Animation' | 'Camera' | 'Event'; keyframes?: Array<{ time: number; value: unknown }>; }>; }): Promise<{ success: true; message: string; path: string } | { success: false; error: string }> { try { const targetPath = params.savePath ?? '/Game/Cinematics'; const validation = validateAssetParams({ name: params.name, savePath: targetPath }); if (!validation.valid) { return { success: false, error: validation.error ?? 'Invalid asset parameters' }; } const sanitized = validation.sanitized; const assetName = sanitized.name; const assetPath = sanitized.savePath ?? targetPath; const commands: string[] = [ `CreateAsset LevelSequence ${assetName} ${assetPath}`, `SetSequenceFrameRate ${assetName} ${params.frameRate ?? 30}`, `SetSequenceDuration ${assetName} ${params.duration ?? 5}` ]; if (params.tracks) { for (const track of params.tracks) { commands.push(`AddSequenceTrack ${assetName} ${track.actorName} ${track.trackType}`); if (track.keyframes) { for (const keyframe of track.keyframes) { commands.push( `AddSequenceKey ${assetName} ${track.actorName} ${track.trackType} ${keyframe.time} ${JSON.stringify(keyframe.value)}` ); } } } } await this.bridge.executeConsoleCommands(commands); return { success: true, message: `Level Sequence ${assetName} created`, path: `${assetPath}/${assetName}` }; } catch (err) { return { success: false, error: `Failed to create level sequence: ${err}` }; } } async playAnimation(params: { actorName: string; animationType: 'Montage' | 'Sequence' | 'BlendSpace'; animationPath: string; playRate?: number; loop?: boolean; blendInTime?: number; blendOutTime?: number; }): Promise<PlayAnimationSuccess | PlayAnimationFailure> { try { const script = this.buildPlayAnimationScript({ actorName: params.actorName, animationType: params.animationType, animationPath: params.animationPath, playRate: params.playRate ?? 1.0, loop: params.loop ?? false, blendInTime: params.blendInTime ?? 0.25, blendOutTime: params.blendOutTime ?? 0.25 }); const response = await this.bridge.executePython(script); const interpreted = interpretStandardResult(response, { successMessage: `Animation ${params.animationType} triggered on ${params.actorName}`, failureMessage: `Failed to play animation on ${params.actorName}` }); const payload = interpreted.payload ?? {}; const warnings = interpreted.warnings ?? coerceStringArray((payload as any).warnings) ?? undefined; const details = interpreted.details ?? coerceStringArray((payload as any).details) ?? undefined; const availableActors = coerceStringArray((payload as any).availableActors); const actorName = coerceString((payload as any).actorName) ?? params.actorName; const animationType = coerceString((payload as any).animationType) ?? params.animationType; const assetPath = coerceString((payload as any).assetPath) ?? params.animationPath; const errorMessage = coerceString((payload as any).error) ?? interpreted.error ?? `Animation playback failed for ${params.actorName}`; if (interpreted.success) { const result: PlayAnimationSuccess = { success: true, message: interpreted.message }; if (warnings && warnings.length > 0) { result.warnings = warnings; } if (details && details.length > 0) { result.details = details; } if (actorName) { result.actorName = actorName; } if (animationType) { result.animationType = animationType; } if (assetPath) { result.assetPath = assetPath; } return result; } const failure: PlayAnimationFailure = { success: false, message: `Failed to play animation: ${errorMessage}`, error: errorMessage }; if (warnings && warnings.length > 0) { failure.warnings = warnings; } if (details && details.length > 0) { failure.details = details; } if (availableActors && availableActors.length > 0) { failure.availableActors = availableActors; } if (actorName) { failure.actorName = actorName; } if (animationType) { failure.animationType = animationType; } if (assetPath) { failure.assetPath = assetPath; } return failure; } catch (err) { const error = `Failed to play animation: ${err}`; return { success: false, message: error, error: String(err) }; } } private buildCreateAnimationBlueprintScript(args: { name: string; path: string; skeletonPath: string; }): string { const payload = JSON.stringify(args); return ` import unreal import json import traceback params = json.loads(${JSON.stringify(payload)}) result = { "success": False, "message": "", "error": "", "warnings": [], "details": [], "exists": False, "skeleton": params.get("skeletonPath") or "" } try: asset_path = (params.get("path") or "/Game").rstrip('/') asset_name = params.get("name") or "" full_path = f"{asset_path}/{asset_name}" result["path"] = full_path editor_lib = unreal.EditorAssetLibrary asset_subsystem = None try: asset_subsystem = unreal.get_editor_subsystem(unreal.EditorAssetSubsystem) except Exception: asset_subsystem = None skeleton_path = params.get("skeletonPath") skeleton_asset = None if skeleton_path: if editor_lib.does_asset_exist(skeleton_path): skeleton_asset = editor_lib.load_asset(skeleton_path) if skeleton_asset and isinstance(skeleton_asset, unreal.Skeleton): result["details"].append(f"Using skeleton: {skeleton_path}") result["skeleton"] = skeleton_path else: result["error"] = f"Skeleton asset invalid at {skeleton_path}" result["warnings"].append(result["error"]) skeleton_asset = None else: result["error"] = f"Skeleton not found at {skeleton_path}" result["warnings"].append(result["error"]) if not skeleton_asset: raise RuntimeError(result["error"] or f"Skeleton {skeleton_path} unavailable") does_exist = False try: if asset_subsystem and hasattr(asset_subsystem, 'does_asset_exist'): does_exist = asset_subsystem.does_asset_exist(full_path) else: does_exist = editor_lib.does_asset_exist(full_path) except Exception: does_exist = editor_lib.does_asset_exist(full_path) if does_exist: result["exists"] = True loaded = editor_lib.load_asset(full_path) if loaded: result["success"] = True result["message"] = f"Animation Blueprint already exists at {full_path}" result["details"].append(result["message"]) else: result["error"] = f"Asset exists but could not be loaded: {full_path}" result["warnings"].append(result["error"]) else: factory = unreal.AnimBlueprintFactory() if skeleton_asset: try: factory.target_skeleton = skeleton_asset except Exception as assign_error: result["warnings"].append(f"Unable to assign skeleton {skeleton_path}: {assign_error}") asset_tools = unreal.AssetToolsHelpers.get_asset_tools() created = asset_tools.create_asset( asset_name=asset_name, package_path=asset_path, asset_class=unreal.AnimBlueprint, factory=factory ) if created: editor_lib.save_asset(full_path, only_if_is_dirty=False) result["success"] = True result["message"] = f"Animation Blueprint created at {full_path}" result["details"].append(result["message"]) else: result["error"] = f"Failed to create Animation Blueprint {asset_name}" except Exception as exc: result["error"] = str(exc) result["warnings"].append(result["error"]) tb = traceback.format_exc() if tb: result.setdefault("details", []).append(tb) if result["success"] and not result.get("message"): result["message"] = f"Animation Blueprint created at {result.get('path')}" if not result["success"] and not result.get("error"): result["error"] = "Animation Blueprint creation failed" if not result.get("warnings"): result.pop("warnings", None) if not result.get("details"): result.pop("details", None) if not result.get("error"): result.pop("error", None) print('RESULT:' + json.dumps(result)) `.trim(); } private parseAnimationBlueprintResponse( response: unknown, assetName: string, assetPath: string ): CreateAnimationBlueprintSuccess | CreateAnimationBlueprintFailure { const interpreted = interpretStandardResult(response, { successMessage: `Animation Blueprint ${assetName} created`, failureMessage: `Failed to create Animation Blueprint ${assetName}` }); const payload = interpreted.payload ?? {}; const path = coerceString((payload as any).path) ?? `${assetPath}/${assetName}`; const exists = coerceBoolean((payload as any).exists); const skeleton = coerceString((payload as any).skeleton); const warnings = interpreted.warnings ?? coerceStringArray((payload as any).warnings) ?? undefined; const details = interpreted.details ?? coerceStringArray((payload as any).details) ?? undefined; if (interpreted.success) { const result: CreateAnimationBlueprintSuccess = { success: true, message: interpreted.message, path }; if (typeof exists === 'boolean') { result.exists = exists; } if (skeleton) { result.skeleton = skeleton; } if (warnings && warnings.length > 0) { result.warnings = warnings; } if (details && details.length > 0) { result.details = details; } return result; } const errorMessage = coerceString((payload as any).error) ?? interpreted.error ?? interpreted.message; const failure: CreateAnimationBlueprintFailure = { success: false, message: `Failed to create Animation Blueprint: ${errorMessage}`, error: errorMessage, path }; if (typeof exists === 'boolean') { failure.exists = exists; } if (skeleton) { failure.skeleton = skeleton; } if (warnings && warnings.length > 0) { failure.warnings = warnings; } if (details && details.length > 0) { failure.details = details; } return failure; } private buildPlayAnimationScript(args: { actorName: string; animationType: string; animationPath: string; playRate: number; loop: boolean; blendInTime: number; blendOutTime: number; }): string { const payload = JSON.stringify(args); return ` import unreal import json import traceback params = json.loads(${JSON.stringify(payload)}) result = { "success": False, "message": "", "error": "", "warnings": [], "details": [], "actorName": params.get("actorName"), "animationType": params.get("animationType"), "assetPath": params.get("animationPath"), "availableActors": [] } try: actor_subsystem = unreal.get_editor_subsystem(unreal.EditorActorSubsystem) actors = actor_subsystem.get_all_level_actors() if actor_subsystem else [] target = None search = params.get("actorName") or "" search_lower = search.lower() for actor in actors: if not actor: continue name = (actor.get_name() or "").lower() label = (actor.get_actor_label() or "").lower() if search_lower and (search_lower == name or search_lower == label or search_lower in label): target = actor break if not target: result["error"] = f"Actor not found: {search}" result["warnings"].append("Actor search yielded no results") suggestions = [] for actor in actors[:20]: try: suggestions.append(actor.get_actor_label()) except Exception: continue if suggestions: result["availableActors"] = suggestions else: try: display_name = target.get_actor_label() or target.get_name() if display_name: result["actorName"] = display_name except Exception: pass skeletal_component = target.get_component_by_class(unreal.SkeletalMeshComponent) if not skeletal_component: try: skeletal_component = target.get_editor_property('mesh') except Exception: skeletal_component = None if not skeletal_component: result["error"] = "No SkeletalMeshComponent found on actor" result["warnings"].append("Actor lacks SkeletalMeshComponent") else: asset_path = params.get("animationPath") if not asset_path or not unreal.EditorAssetLibrary.does_asset_exist(asset_path): result["error"] = f"Animation asset not found: {asset_path}" result["warnings"].append("Animation asset missing") else: asset = unreal.EditorAssetLibrary.load_asset(asset_path) anim_type = params.get("animationType") or "" if anim_type == 'Montage': anim_instance = skeletal_component.get_anim_instance() if anim_instance: try: anim_instance.montage_play(asset, params.get("playRate", 1.0)) result["success"] = True result["message"] = f"Montage playing on {result.get('actorName') or search}" result["details"].append(result["message"]) except Exception as play_error: result["error"] = f"Failed to play montage: {play_error}" result["warnings"].append(result["error"]) else: result["error"] = "AnimInstance not found on SkeletalMeshComponent" result["warnings"].append(result["error"]) elif anim_type == 'Sequence': try: skeletal_component.play_animation(asset, bool(params.get("loop"))) try: anim_instance = skeletal_component.get_anim_instance() if anim_instance: anim_instance.set_play_rate(params.get("playRate", 1.0)) except Exception: pass result["success"] = True result["message"] = f"Sequence playing on {result.get('actorName') or search}" result["details"].append(result["message"]) except Exception as play_error: result["error"] = f"Failed to play sequence: {play_error}" result["warnings"].append(result["error"]) else: result["error"] = "BlendSpace playback requires Animation Blueprint support" result["warnings"].append("Unsupported animation type for direct play") except Exception as exc: result["error"] = str(exc) result["warnings"].append(result["error"]) tb = traceback.format_exc() if tb: result["details"].append(tb) if result["success"] and not result.get("message"): result["message"] = f"Animation {result.get('animationType')} triggered on {result.get('actorName') or params.get('actorName')}" if not result["success"] and not result.get("error"): result["error"] = "Animation playback failed" if not result.get("warnings"): result.pop("warnings", None) if not result.get("details"): result.pop("details", None) if not result.get("availableActors"): result.pop("availableActors", None) if not result.get("error"): result.pop("error", None) print('RESULT:' + json.dumps(result)) `.trim(); } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/ChiR24/Unreal_mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server