Skip to main content
Glama
ui.ts18.3 kB
// UI tools for Unreal Engine import { UnrealBridge } from '../unreal-bridge.js'; import { bestEffortInterpretedText, interpretStandardResult } from '../utils/result-helpers.js'; export class UITools { constructor(private bridge: UnrealBridge) {} // Create widget blueprint async createWidget(params: { name: string; type?: 'HUD' | 'Menu' | 'Inventory' | 'Dialog' | 'Custom'; savePath?: string; }) { const path = params.savePath || '/Game/UI/Widgets'; const py = ` import unreal import json name = r"${params.name}" path = r"${path}" try: asset_tools = unreal.AssetToolsHelpers.get_asset_tools() try: factory = unreal.WidgetBlueprintFactory() except Exception: factory = None if not factory: print('RESULT:' + json.dumps({'success': False, 'error': 'WidgetBlueprintFactory unavailable'})) else: # Try setting parent_class in a version-tolerant way try: factory.parent_class = unreal.UserWidget except Exception: try: factory.set_editor_property('parent_class', unreal.UserWidget) except Exception: pass asset = asset_tools.create_asset(asset_name=name, package_path=path, asset_class=unreal.WidgetBlueprint, factory=factory) if asset: unreal.EditorAssetLibrary.save_asset(f"{path}/{name}") print('RESULT:' + json.dumps({'success': True})) else: print('RESULT:' + json.dumps({'success': False, 'error': 'Failed to create WidgetBlueprint'})) except Exception as e: print('RESULT:' + json.dumps({'success': False, 'error': str(e)})) `.trim(); try { const resp = await this.bridge.executePython(py); const interpreted = interpretStandardResult(resp, { successMessage: 'Widget blueprint created', failureMessage: 'Failed to create WidgetBlueprint' }); if (interpreted.success) { return { success: true, message: interpreted.message }; } return { success: false, error: interpreted.error ?? 'Failed to create widget blueprint', details: bestEffortInterpretedText(interpreted) }; } catch (e) { return { success: false, error: `Failed to create widget blueprint: ${e}` }; } } // Add widget component async addWidgetComponent(params: { widgetName: string; componentType: 'Button' | 'Text' | 'Image' | 'ProgressBar' | 'Slider' | 'CheckBox' | 'ComboBox' | 'TextBox' | 'ScrollBox' | 'Canvas' | 'VerticalBox' | 'HorizontalBox' | 'Grid' | 'Overlay'; componentName: string; slot?: { position?: [number, number]; size?: [number, number]; anchor?: [number, number, number, number]; alignment?: [number, number]; }; }) { const commands: string[] = []; commands.push(`AddWidgetComponent ${params.widgetName} ${params.componentType} ${params.componentName}`); if (params.slot) { if (params.slot.position) { commands.push(`SetWidgetPosition ${params.widgetName}.${params.componentName} ${params.slot.position.join(' ')}`); } if (params.slot.size) { commands.push(`SetWidgetSize ${params.widgetName}.${params.componentName} ${params.slot.size.join(' ')}`); } if (params.slot.anchor) { commands.push(`SetWidgetAnchor ${params.widgetName}.${params.componentName} ${params.slot.anchor.join(' ')}`); } if (params.slot.alignment) { commands.push(`SetWidgetAlignment ${params.widgetName}.${params.componentName} ${params.slot.alignment.join(' ')}`); } } await this.bridge.executeConsoleCommands(commands); return { success: true, message: `Component ${params.componentName} added to widget` }; } // Set text async setWidgetText(params: { widgetName: string; componentName: string; text: string; fontSize?: number; color?: [number, number, number, number]; fontFamily?: string; }) { const commands: string[] = []; commands.push(`SetWidgetText ${params.widgetName}.${params.componentName} "${params.text}"`); if (params.fontSize !== undefined) { commands.push(`SetWidgetFontSize ${params.widgetName}.${params.componentName} ${params.fontSize}`); } if (params.color) { commands.push(`SetWidgetTextColor ${params.widgetName}.${params.componentName} ${params.color.join(' ')}`); } if (params.fontFamily) { commands.push(`SetWidgetFont ${params.widgetName}.${params.componentName} ${params.fontFamily}`); } await this.bridge.executeConsoleCommands(commands); return { success: true, message: 'Widget text updated' }; } // Set image async setWidgetImage(params: { widgetName: string; componentName: string; imagePath: string; tint?: [number, number, number, number]; sizeToContent?: boolean; }) { const commands: string[] = []; commands.push(`SetWidgetImage ${params.widgetName}.${params.componentName} ${params.imagePath}`); if (params.tint) { commands.push(`SetWidgetImageTint ${params.widgetName}.${params.componentName} ${params.tint.join(' ')}`); } if (params.sizeToContent !== undefined) { commands.push(`SetWidgetSizeToContent ${params.widgetName}.${params.componentName} ${params.sizeToContent}`); } await this.bridge.executeConsoleCommands(commands); return { success: true, message: 'Widget image updated' }; } // Create HUD async createHUD(params: { name: string; elements?: Array<{ type: 'HealthBar' | 'AmmoCounter' | 'Score' | 'Timer' | 'Minimap' | 'Crosshair'; position: [number, number]; size?: [number, number]; }>; }) { const commands: string[] = []; commands.push(`CreateHUDClass ${params.name}`); if (params.elements) { for (const element of params.elements) { const size = element.size || [100, 50]; commands.push(`AddHUDElement ${params.name} ${element.type} ${element.position.join(' ')} ${size.join(' ')}`); } } await this.bridge.executeConsoleCommands(commands); return { success: true, message: `HUD ${params.name} created` }; } // Show/Hide widget async setWidgetVisibility(params: { widgetName: string; visible: boolean; playerIndex?: number; }) { const playerIndex = params.playerIndex ?? 0; const widgetName = params.widgetName?.trim(); if (!widgetName) { return { success: false, error: 'widgetName is required' }; } const verifyScript = ` import unreal, json name = r"${widgetName}" candidates = [] if name.startswith('/Game/'): candidates.append(name) else: candidates.append(f"/Game/UI/Widgets/{name}") candidates.append(f"/Game/{name}") found_path = '' for path in candidates: if unreal.EditorAssetLibrary.does_asset_exist(path): found_path = path break print('RESULT:' + json.dumps({'success': bool(found_path), 'path': found_path, 'candidates': candidates})) `.trim(); const verify = await this.bridge.executePythonWithResult(verifyScript); if (!verify?.success) { return { success: false, error: `Widget asset not found for ${widgetName}` }; } const command = params.visible ? `ShowWidget ${widgetName} ${playerIndex}` : `HideWidget ${widgetName} ${playerIndex}`; const raw = await this.bridge.executeConsoleCommand(command); const summary = this.bridge.summarizeConsoleCommand(command, raw); return { success: true, message: params.visible ? `Widget ${widgetName} show command issued` : `Widget ${widgetName} hide command issued`, command: summary.command, output: summary.output || undefined, logLines: summary.logLines?.length ? summary.logLines : undefined }; } // Add widget to viewport async addWidgetToViewport(params: { widgetClass: string; zOrder?: number; playerIndex?: number; }) { const zOrder = params.zOrder ?? 0; const playerIndex = params.playerIndex ?? 0; // Use Python API to create and add widget to viewport const py = ` import unreal import json widget_path = r"${params.widgetClass}" z_order = ${zOrder} player_index = ${playerIndex} try: # Load the widget blueprint class if not unreal.EditorAssetLibrary.does_asset_exist(widget_path): print('RESULT:' + json.dumps({'success': False, 'error': f'Widget class not found: {widget_path}'})) else: widget_bp = unreal.EditorAssetLibrary.load_asset(widget_path) if not widget_bp: print('RESULT:' + json.dumps({'success': False, 'error': 'Failed to load widget blueprint'})) else: # Get the generated class from the widget blueprint widget_class = widget_bp.generated_class() if hasattr(widget_bp, 'generated_class') else widget_bp # Get the world and player controller via modern subsystems world = None try: world = unreal.EditorUtilityLibrary.get_editor_world() except Exception: pass if not world: editor_subsystem = unreal.get_editor_subsystem(unreal.UnrealEditorSubsystem) if editor_subsystem and hasattr(editor_subsystem, 'get_editor_world'): world = editor_subsystem.get_editor_world() if not world: print('RESULT:' + json.dumps({'success': False, 'error': 'No editor world available. Start a PIE session or enable Editor Scripting Utilities.'})) else: # Try to get player controller try: player_controller = unreal.GameplayStatics.get_player_controller(world, player_index) except Exception: player_controller = None if not player_controller: # If no player controller in PIE, try to get the first one or create a dummy print('RESULT:' + json.dumps({'success': False, 'error': 'No player controller available. Run in PIE mode first.'})) else: # Create the widget widget = unreal.WidgetBlueprintLibrary.create(world, widget_class, player_controller) if widget: # Add to viewport widget.add_to_viewport(z_order) print('RESULT:' + json.dumps({'success': True})) else: print('RESULT:' + json.dumps({'success': False, 'error': 'Failed to create widget instance'})) except Exception as e: print('RESULT:' + json.dumps({'success': False, 'error': str(e)})) `.trim(); try { const resp = await this.bridge.executePython(py); const interpreted = interpretStandardResult(resp, { successMessage: `Widget added to viewport with z-order ${zOrder}`, failureMessage: 'Failed to add widget to viewport' }); if (interpreted.success) { return { success: true, message: interpreted.message }; } return { success: false, error: interpreted.error ?? 'Failed to add widget to viewport', details: bestEffortInterpretedText(interpreted) }; } catch (e) { return { success: false, error: `Failed to add widget to viewport: ${e}` }; } } // Remove widget from viewport async removeWidgetFromViewport(params: { widgetName: string; playerIndex?: number; }) { const playerIndex = params.playerIndex ?? 0; const command = `RemoveWidgetFromViewport ${params.widgetName} ${playerIndex}`; return this.bridge.executeConsoleCommand(command); } // Create menu async createMenu(params: { name: string; menuType: 'Main' | 'Pause' | 'Settings' | 'Inventory'; buttons?: Array<{ text: string; action: string; position?: [number, number]; }>; }) { const commands: string[] = []; commands.push(`CreateMenuWidget ${params.name} ${params.menuType}`); if (params.buttons) { for (const button of params.buttons) { const pos = button.position || [0, 0]; commands.push(`AddMenuButton ${params.name} "${button.text}" ${button.action} ${pos.join(' ')}`); } } await this.bridge.executeConsoleCommands(commands); return { success: true, message: `Menu ${params.name} created` }; } // Set widget animation async createWidgetAnimation(params: { widgetName: string; animationName: string; duration: number; tracks?: Array<{ componentName: string; property: 'Position' | 'Scale' | 'Rotation' | 'Opacity' | 'Color'; keyframes: Array<{ time: number; value: number | [number, number] | [number, number, number] | [number, number, number, number]; }>; }>; }) { const commands: string[] = []; commands.push(`CreateWidgetAnimation ${params.widgetName} ${params.animationName} ${params.duration}`); if (params.tracks) { for (const track of params.tracks) { commands.push(`AddAnimationTrack ${params.widgetName}.${params.animationName} ${track.componentName} ${track.property}`); for (const keyframe of track.keyframes) { const value = Array.isArray(keyframe.value) ? keyframe.value.join(' ') : keyframe.value; commands.push(`AddAnimationKeyframe ${params.widgetName}.${params.animationName} ${track.componentName} ${keyframe.time} ${value}`); } } } await this.bridge.executeConsoleCommands(commands); return { success: true, message: `Animation ${params.animationName} created` }; } // Play widget animation async playWidgetAnimation(params: { widgetName: string; animationName: string; playMode?: 'Forward' | 'Reverse' | 'PingPong'; loops?: number; }) { const playMode = params.playMode || 'Forward'; const loops = params.loops ?? 1; const command = `PlayWidgetAnimation ${params.widgetName} ${params.animationName} ${playMode} ${loops}`; return this.bridge.executeConsoleCommand(command); } // Set widget style async setWidgetStyle(params: { widgetName: string; componentName: string; style: { backgroundColor?: [number, number, number, number]; borderColor?: [number, number, number, number]; borderWidth?: number; padding?: [number, number, number, number]; margin?: [number, number, number, number]; }; }) { const commands: string[] = []; if (params.style.backgroundColor) { commands.push(`SetWidgetBackgroundColor ${params.widgetName}.${params.componentName} ${params.style.backgroundColor.join(' ')}`); } if (params.style.borderColor) { commands.push(`SetWidgetBorderColor ${params.widgetName}.${params.componentName} ${params.style.borderColor.join(' ')}`); } if (params.style.borderWidth !== undefined) { commands.push(`SetWidgetBorderWidth ${params.widgetName}.${params.componentName} ${params.style.borderWidth}`); } if (params.style.padding) { commands.push(`SetWidgetPadding ${params.widgetName}.${params.componentName} ${params.style.padding.join(' ')}`); } if (params.style.margin) { commands.push(`SetWidgetMargin ${params.widgetName}.${params.componentName} ${params.style.margin.join(' ')}`); } await this.bridge.executeConsoleCommands(commands); return { success: true, message: 'Widget style updated' }; } // Bind widget event async bindWidgetEvent(params: { widgetName: string; componentName: string; eventType: 'OnClicked' | 'OnPressed' | 'OnReleased' | 'OnHovered' | 'OnUnhovered' | 'OnTextChanged' | 'OnTextCommitted' | 'OnValueChanged'; functionName: string; }) { const command = `BindWidgetEvent ${params.widgetName}.${params.componentName} ${params.eventType} ${params.functionName}`; return this.bridge.executeConsoleCommand(command); } // Set input mode async setInputMode(params: { mode: 'GameOnly' | 'UIOnly' | 'GameAndUI'; showCursor?: boolean; lockCursor?: boolean; }) { const commands: string[] = []; commands.push(`SetInputMode ${params.mode}`); if (params.showCursor !== undefined) { commands.push(`ShowMouseCursor ${params.showCursor}`); } if (params.lockCursor !== undefined) { commands.push(`SetMouseLockMode ${params.lockCursor}`); } await this.bridge.executeConsoleCommands(commands); return { success: true, message: `Input mode set to ${params.mode}` }; } // Create tooltip async createTooltip(params: { widgetName: string; componentName: string; text: string; delay?: number; }) { const delay = params.delay ?? 0.5; const command = `SetWidgetTooltip ${params.widgetName}.${params.componentName} "${params.text}" ${delay}`; return this.bridge.executeConsoleCommand(command); } // Create drag and drop async setupDragDrop(params: { widgetName: string; componentName: string; dragVisual?: string; dropTargets?: string[]; }) { const commands = []; commands.push(`EnableDragDrop ${params.widgetName}.${params.componentName}`); if (params.dragVisual) { commands.push(`SetDragVisual ${params.widgetName}.${params.componentName} ${params.dragVisual}`); } if (params.dropTargets) { for (const target of params.dropTargets) { commands.push(`AddDropTarget ${params.widgetName}.${params.componentName} ${target}`); } } await this.bridge.executeConsoleCommands(commands); return { success: true, message: 'Drag and drop configured' }; } // Create notification async showNotification(params: { text: string; duration?: number; type?: 'Info' | 'Success' | 'Warning' | 'Error'; position?: 'TopLeft' | 'TopCenter' | 'TopRight' | 'BottomLeft' | 'BottomCenter' | 'BottomRight'; }) { const duration = params.duration ?? 3.0; const type = params.type || 'Info'; const position = params.position || 'TopRight'; const command = `ShowNotification "${params.text}" ${duration} ${type} ${position}`; return this.bridge.executeConsoleCommand(command); } }

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