diff --git a/core/script_system.py b/core/script_system.py index 89b1073e..e7686f3c 100644 --- a/core/script_system.py +++ b/core/script_system.py @@ -1,858 +1,861 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -脚本系统模块 - -负责脚本的创建、管理、挂载和运行: -- 脚本管理器:统一管理所有脚本 -- 脚本组件:挂载到游戏对象的脚本实例 -- 脚本引擎:执行脚本逻辑 -- 脚本API:提供给脚本使用的API接口 -""" - -import os -import sys -import importlib -import importlib.util -import traceback -import inspect -import time -from typing import Dict, List, Any, Optional, Callable -from abc import ABC, abstractmethod -from direct.task.TaskManagerGlobal import taskMgr -from panda3d.core import PythonTask - - -class ScriptBase(ABC): - """脚本基类 - 所有用户脚本都应该继承此类""" - - def __init__(self): - self.enabled = True - self.transform = None # 挂载的对象的transform - self.gameObject = None # 挂载的游戏对象 - self.world = None # 引擎世界对象引用 - self._script_id = None - - @abstractmethod - def start(self): - """脚本开始时调用(类似Unity的Start)""" - pass - - @abstractmethod - def update(self, dt): - """每帧更新时调用(类似Unity的Update)""" - pass - - def on_destroy(self): - """脚本销毁时调用(类似Unity的OnDestroy)""" - pass - - def on_enable(self): - """脚本启用时调用""" - pass - - def on_disable(self): - """脚本禁用时调用""" - pass - - def on_collision_enter(self, other): - """碰撞开始时调用""" - pass - - def on_collision_exit(self, other): - """碰撞结束时调用""" - pass - - def log(self, message): - """日志输出""" - print(f"[{self.__class__.__name__}] {message}") - - -class ScriptComponent: - """脚本组件 - 挂载到游戏对象上的脚本实例""" - - def __init__(self, script_instance: ScriptBase, game_object, script_manager): - self.script_instance = script_instance - self.game_object = game_object - self.script_manager = script_manager - self.enabled = True - - # 保存脚本名称,便于UI显示 - self.script_name = script_instance.__class__.__name__ - - # 设置脚本实例的引用 - script_instance.gameObject = game_object - script_instance.transform = game_object # Panda3D中NodePath就是transform - script_instance.world = script_manager.world - script_instance._script_id = id(self) - - # 标记脚本已开始 - self._started = False - - def start(self): - """启动脚本""" - if not self._started and self.enabled: - try: - self.script_instance.start() - self._started = True - except Exception as e: - print(f"脚本启动失败: {e}") - traceback.print_exc() - - def update(self, dt): - """更新脚本""" - if self.enabled and self._started: - try: - self.script_instance.update(dt) - except Exception as e: - print(f"脚本更新失败: {e}") - traceback.print_exc() - - def destroy(self): - """销毁脚本""" - try: - self.script_instance.on_destroy() - except Exception as e: - print(f"脚本销毁失败: {e}") - traceback.print_exc() - - def set_enabled(self, enabled): - """设置脚本启用状态""" - if self.enabled != enabled: - self.enabled = enabled - try: - if enabled: - self.script_instance.on_enable() - else: - self.script_instance.on_disable() - except Exception as e: - print(f"设置脚本状态失败: {e}") - traceback.print_exc() - - -class ScriptEngine: - """脚本引擎 - 负责脚本的执行和生命周期管理""" - - def __init__(self, world): - self.world = world - self.script_components: List[ScriptComponent] = [] - self.update_task = None - - def start_engine(self): - """启动脚本引擎""" - if self.update_task is None: - self.update_task = taskMgr.add(self._update_scripts, "script_update") - print("✓ 脚本引擎已启动") - - def stop_engine(self): - """停止脚本引擎""" - if self.update_task: - taskMgr.remove(self.update_task) - self.update_task = None - print("✓ 脚本引擎已停止") - - def add_script_component(self, script_component: ScriptComponent): - """添加脚本组件""" - self.script_components.append(script_component) - # 如果引擎已运行,立即启动脚本 - if self.update_task: - script_component.start() - - def remove_script_component(self, script_component: ScriptComponent): - """移除脚本组件""" - if script_component in self.script_components: - script_component.destroy() - self.script_components.remove(script_component) - - def _update_scripts(self, task): - """更新所有脚本(每帧调用)""" - from direct.showbase.ShowBaseGlobal import globalClock - dt = globalClock.getDt() - - # 复制列表以避免迭代时修改 - components_copy = self.script_components.copy() - - for component in components_copy: - if component.enabled: - # 如果脚本还没开始,先调用start - if not component._started: - component.start() - # 然后调用update - component.update(dt) - - return task.cont - - -class ScriptLoader: - """脚本加载器 - 负责加载和重载脚本""" - - def __init__(self, script_manager): - self.script_manager = script_manager - self.loaded_modules = {} # 模块名 -> 模块对象 - self.script_classes = {} # 脚本名 -> 脚本类 - self.file_mtimes = {} # 文件路径 -> 修改时间 - - def load_script_from_file(self, script_path: str) -> Optional[type]: - """从文件加载脚本类""" - try: - if not os.path.exists(script_path): - print(f"脚本文件不存在: {script_path}") - return None - - # 获取脚本名称(不包含扩展名) - script_name = os.path.splitext(os.path.basename(script_path))[0] - - # 动态导入模块 - spec = importlib.util.spec_from_file_location(script_name, script_path) - if spec is None: - print(f"无法创建模块规范: {script_path}") - return None - - module = importlib.util.module_from_spec(spec) - - # 如果模块已经加载过,先卸载 - if script_name in self.loaded_modules: - self.unload_script(script_name) - - # 执行模块 - spec.loader.exec_module(module) - - # 查找继承自ScriptBase的类 - script_class = None - for name, obj in inspect.getmembers(module, inspect.isclass): - if issubclass(obj, ScriptBase) and obj != ScriptBase: - script_class = obj - break - - if script_class is None: - print(f"脚本文件中没有找到继承自ScriptBase的类: {script_path}") - return None - - # 保存模块和类信息 - self.loaded_modules[script_name] = module - self.script_classes[script_name] = script_class - self.file_mtimes[script_path] = os.path.getmtime(script_path) - - print(f"✓ 成功加载脚本: {script_name} 从 {script_path}") - return script_class - - except Exception as e: - print(f"加载脚本失败 {script_path}: {e}") - traceback.print_exc() - return None - - def unload_script(self, script_name: str): - """卸载脚本""" - if script_name in self.loaded_modules: - # 移除所有使用此脚本的组件 - components_to_remove = [] - for component in self.script_manager.engine.script_components: - if component.script_instance.__class__.__name__ == script_name: - components_to_remove.append(component) - - for component in components_to_remove: - self.script_manager.remove_script_from_object(component.game_object, script_name) - - # 从sys.modules中移除 - module = self.loaded_modules[script_name] - if module.__name__ in sys.modules: - del sys.modules[module.__name__] - - # 清理引用 - del self.loaded_modules[script_name] - if script_name in self.script_classes: - del self.script_classes[script_name] - - print(f"✓ 脚本已卸载: {script_name}") - - def reload_script(self, script_path: str) -> Optional[type]: - """重新加载脚本(热重载)""" - script_name = os.path.splitext(os.path.basename(script_path))[0] - print(f"重新加载脚本: {script_name}") - - # 先卸载旧版本 - if script_name in self.loaded_modules: - self.unload_script(script_name) - - # 重新加载 - return self.load_script_from_file(script_path) - - def check_for_changes(self): - """检查脚本文件是否有变化(用于热重载)""" - changed_scripts = [] - - for script_path, old_mtime in self.file_mtimes.items(): - if os.path.exists(script_path): - current_mtime = os.path.getmtime(script_path) - if current_mtime > old_mtime: - changed_scripts.append(script_path) - - # 重新加载变化的脚本 - for script_path in changed_scripts: - self.reload_script(script_path) - - return len(changed_scripts) > 0 - - def find_script_file(self, script_name: str) -> Optional[str]: - """根据脚本名称查找脚本文件路径""" - # 首先检查已加载的脚本 - if script_name in self.loaded_modules: - module = self.loaded_modules[script_name] - if hasattr(module, '__file__') and module.__file__: - return module.__file__ - - # 在已知的文件路径中查找 - for file_path in self.file_mtimes.keys(): - file_name = os.path.splitext(os.path.basename(file_path))[0] - if file_name == script_name: - return file_path - - # 在脚本目录中查找 - scripts_dir = self.script_manager.scripts_directory - if os.path.exists(scripts_dir): - for file_name in os.listdir(scripts_dir): - if file_name.endswith('.py'): - base_name = os.path.splitext(file_name)[0] - if base_name == script_name: - return os.path.join(scripts_dir, file_name) - - return None - - -class ScriptAPI: - """脚本API - 提供给脚本使用的API接口""" - - def __init__(self, world): - self.world = world - - # ==================== 游戏对象操作 ==================== - - def find_object_by_name(self, name: str): - """根据名称查找游戏对象""" - return self.world.render.find(name) - - def create_object(self, name: str = "GameObject"): - """创建游戏对象""" - obj = self.world.render.attachNewNode(name) - return obj - - def destroy_object(self, obj): - """销毁游戏对象""" - if obj: - obj.removeNode() - - # ==================== 变换操作 ==================== - - def get_position(self, obj): - """获取对象位置""" - return obj.getPos() if obj else None - - def set_position(self, obj, x, y, z): - """设置对象位置""" - if obj: - obj.setPos(x, y, z) - - def get_rotation(self, obj): - """获取对象旋转""" - return obj.getHpr() if obj else None - - def set_rotation(self, obj, h, p, r): - """设置对象旋转""" - if obj: - obj.setHpr(h, p, r) - - def get_scale(self, obj): - """获取对象缩放""" - return obj.getScale() if obj else None - - def set_scale(self, obj, sx, sy, sz): - """设置对象缩放""" - if obj: - obj.setScale(sx, sy, sz) - - # ==================== 输入系统 ==================== - - def is_key_pressed(self, key): - """检查按键是否被按下""" - # 这里需要集成到现有的输入系统 - return False # 暂时返回False - - # ==================== 时间系统 ==================== - - def get_time(self): - """获取游戏时间""" - return time.time() - - def get_delta_time(self): - """获取帧间隔时间""" - from direct.showbase.ShowBaseGlobal import globalClock - return globalClock.getDt() - - # ==================== 日志系统 ==================== - - def log(self, message): - """输出日志""" - print(f"[ScriptAPI] {message}") - - -class ScriptManager: - """脚本管理器 - 统一管理所有脚本功能""" - - def __init__(self, world): - """初始化脚本管理器 - - Args: - world: 主程序world对象引用 - """ - self.world = world - - # 初始化子系统 - self.engine = ScriptEngine(world) - self.loader = ScriptLoader(self) - self.api = ScriptAPI(world) - - # 脚本存储 - self.object_scripts: Dict[Any, List[ScriptComponent]] = {} # 对象 -> 脚本组件列表 - self.script_templates: Dict[str, type] = {} # 脚本名 -> 脚本类 - - # 脚本目录 - self.scripts_directory = "scripts" - self._ensure_scripts_directory() - - # 热重载监控 - self.hot_reload_enabled = True - self.hot_reload_task = None - - print("✓ 脚本管理系统初始化完成") - - def _ensure_scripts_directory(self): - """确保脚本目录存在""" - if not os.path.exists(self.scripts_directory): - os.makedirs(self.scripts_directory) - - # 创建示例脚本 - self._create_example_script() - - def _create_example_script(self): - """创建示例脚本""" - example_script = '''#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -示例脚本 - 演示如何编写脚本 -""" - -from core.script_system import ScriptBase - -class ExampleScript(ScriptBase): - """示例脚本类""" - - def __init__(self): - super().__init__() - self.counter = 0 - self.rotation_speed = 30.0 # 度/秒 - - def start(self): - """脚本开始时调用""" - self.log("示例脚本开始运行!") - self.log(f"挂载到对象: {self.gameObject.getName()}") - - def update(self, dt): - """每帧更新""" - self.counter += 1 - - # 每60帧输出一次信息 - if self.counter % 60 == 0: - self.log(f"运行了 {self.counter} 帧") - - # 让对象旋转 - if self.transform: - current_h = self.transform.getH() - new_h = current_h + self.rotation_speed * dt - self.transform.setH(new_h) - - def on_destroy(self): - """脚本销毁时调用""" - self.log("示例脚本被销毁") - - def on_enable(self): - """脚本启用时调用""" - self.log("示例脚本被启用") - - def on_disable(self): - """脚本禁用时调用""" - self.log("示例脚本被禁用") -''' - - example_path = os.path.join(self.scripts_directory, "example_script.py") - with open(example_path, 'w', encoding='utf-8') as f: - f.write(example_script) - - print(f"✓ 创建示例脚本: {example_path}") - - # ==================== 脚本管理功能 ==================== - - def start_system(self): - """启动脚本系统""" - self.engine.start_engine() - - if self.hot_reload_enabled: - self.start_hot_reload() - - print("✓ 脚本系统已启动") - - def stop_system(self): - """停止脚本系统""" - self.engine.stop_engine() - self.stop_hot_reload() - print("✓ 脚本系统已停止") - - def start_hot_reload(self): - """启动热重载监控""" - if self.hot_reload_task is None: - self.hot_reload_task = taskMgr.add(self._check_hot_reload, "script_hot_reload") - print("✓ 脚本热重载监控已启动") - - def stop_hot_reload(self): - """停止热重载监控""" - if self.hot_reload_task: - taskMgr.remove(self.hot_reload_task) - self.hot_reload_task = None - print("✓ 脚本热重载监控已停止") - - def _check_hot_reload(self, task): - """检查热重载(每秒调用一次)""" - self.loader.check_for_changes() - task.delayTime = 1.0 # 1秒后再次调用 - return task.again - - # ==================== 脚本创建和加载 ==================== - - def create_script_file(self, script_name: str, template: str = "basic") -> str: - """创建新的脚本文件""" - script_path = os.path.join(self.scripts_directory, f"{script_name}.py") - - if os.path.exists(script_path): - print(f"脚本文件已存在: {script_path}") - return script_path - - # 根据模板创建脚本 - if template == "basic": - script_content = self._get_basic_script_template(script_name) - elif template == "movement": - script_content = self._get_movement_script_template(script_name) - else: - script_content = self._get_basic_script_template(script_name) - - with open(script_path, 'w', encoding='utf-8') as f: - f.write(script_content) - - print(f"✓ 创建脚本文件: {script_path}") - return script_path - - def _get_basic_script_template(self, script_name: str) -> str: - """获取基础脚本模板""" - class_name = ''.join(word.capitalize() for word in script_name.split('_')) - - return f'''#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -{script_name} - 自定义脚本 -""" - -from core.script_system import ScriptBase - -class {class_name}(ScriptBase): - """自定义脚本类""" - - def __init__(self): - super().__init__() - # 在这里初始化您的变量 - - def start(self): - """脚本开始时调用""" - self.log("脚本开始运行!") - - def update(self, dt): - """每帧更新""" - # 在这里编写更新逻辑 - pass - - def on_destroy(self): - """脚本销毁时调用""" - self.log("脚本被销毁") -''' - - def _get_movement_script_template(self, script_name: str) -> str: - """获取移动脚本模板""" - class_name = ''.join(word.capitalize() for word in script_name.split('_')) - - return f'''#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -{script_name} - 移动脚本 -""" - -from core.script_system import ScriptBase - -class {class_name}(ScriptBase): - """移动脚本类""" - - def __init__(self): - super().__init__() - self.speed = 5.0 # 移动速度 - self.direction = [1, 0, 0] # 移动方向 - - def start(self): - """脚本开始时调用""" - self.log("移动脚本开始运行!") - - def update(self, dt): - """每帧更新""" - if self.transform: - # 计算移动偏移 - offset_x = self.direction[0] * self.speed * dt - offset_y = self.direction[1] * self.speed * dt - offset_z = self.direction[2] * self.speed * dt - - # 更新位置 - current_pos = self.transform.getPos() - new_pos = ( - current_pos.x + offset_x, - current_pos.y + offset_y, - current_pos.z + offset_z - ) - self.transform.setPos(*new_pos) - - def on_destroy(self): - """脚本销毁时调用""" - self.log("移动脚本被销毁") -''' - - def load_script_from_file(self, script_path: str) -> Optional[type]: - """从文件加载脚本""" - return self.loader.load_script_from_file(script_path) - - def load_all_scripts_from_directory(self, directory: str = None) -> List[str]: - """从目录加载所有脚本""" - if directory is None: - directory = self.scripts_directory - - if not os.path.exists(directory): - print(f"脚本目录不存在: {directory}") - return [] - - loaded_scripts = [] - for filename in os.listdir(directory): - if filename.endswith('.py') and not filename.startswith('__'): - script_path = os.path.join(directory, filename) - script_class = self.load_script_from_file(script_path) - if script_class: - script_name = os.path.splitext(filename)[0] - loaded_scripts.append(script_name) - - print(f"✓ 从目录 {directory} 加载了 {len(loaded_scripts)} 个脚本") - return loaded_scripts - - # ==================== 脚本挂载和管理 ==================== - - def add_script_to_object(self, game_object, script_name: str) -> Optional[ScriptComponent]: - """为对象添加脚本""" - # 查找脚本类 - script_class = self.loader.script_classes.get(script_name) - if script_class is None: - print(f"未找到脚本类: {script_name}") - return None - - try: - # 创建脚本实例 - script_instance = script_class() - - # 创建脚本组件 - script_component = ScriptComponent(script_instance, game_object, self) - - # 添加到对象的脚本列表 - if game_object not in self.object_scripts: - self.object_scripts[game_object] = [] - self.object_scripts[game_object].append(script_component) - - # 添加到脚本引擎 - self.engine.add_script_component(script_component) - - print(f"✓ 为对象 {game_object.getName()} 添加脚本: {script_name}") - return script_component - - except Exception as e: - print(f"添加脚本失败: {e}") - traceback.print_exc() - return None - - def remove_script_from_object(self, game_object, script_name: str) -> bool: - """从游戏对象移除脚本""" - if game_object not in self.object_scripts: - return False - - script_components = self.object_scripts[game_object] - removed = False - - for component in script_components[:]: # 复制列表以避免修改时出错 - if component.script_instance.__class__.__name__ == script_name: - # 从引擎移除 - self.engine.remove_script_component(component) - # 从对象脚本列表移除 - script_components.remove(component) - removed = True - - print(f"✓ 从对象 {game_object.getName()} 移除脚本: {script_name}") - - - if not script_components: - del self.object_scripts[game_object] - - # 更新节点上保存的脚本信息标签 - if removed: - self._update_node_script_tags_after_removal(game_object, script_name) - - return removed - - def _update_node_script_tags_after_removal(self, game_object, removed_script_name): - """在移除脚本后更新节点标签""" - try: - # 获取对象上剩余的脚本 - remaining_scripts = self.get_scripts_on_object(game_object) - - if not remaining_scripts: - # 如果没有其他脚本,清除所有脚本标签 - if game_object.hasTag("has_scripts"): - game_object.clearTag("has_scripts") - if game_object.hasTag("scripts_info"): - game_object.clearTag("scripts_info") - print(f"✓ 清除节点 {game_object.getName()} 的所有脚本标签") - else: - # 如果还有其他脚本,更新脚本信息标签 - script_info_list = [] - for script_component in remaining_scripts: - script_name = script_component.script_name - script_class = script_component.script_instance.__class__ - script_file = self.loader.find_script_file(script_name) or "" - - script_info_list.append({ - "name": script_name, - "file": script_file - }) - - import json - game_object.setTag("has_scripts", "true") - game_object.setTag("scripts_info", json.dumps(script_info_list, ensure_ascii=False)) - print(f"✓ 更新节点 {game_object.getName()} 的脚本标签信息,剩余 {len(script_info_list)} 个脚本") - - except Exception as e: - print(f"更新节点标签失败: {e}") - - def get_scripts_on_object(self, game_object) -> List[ScriptComponent]: - """获取对象上的所有脚本""" - return self.object_scripts.get(game_object, []) - - def get_script_on_object(self, game_object, script_name: str) -> Optional[ScriptComponent]: - """获取对象上的特定脚本""" - scripts = self.get_scripts_on_object(game_object) - for script in scripts: - if script.script_instance.__class__.__name__ == script_name: - return script - return None - - # ==================== 脚本信息查询 ==================== - - def get_available_scripts(self) -> List[str]: - """获取所有可用的脚本名称""" - return list(self.loader.script_classes.keys()) - - def get_script_info(self, script_name: str) -> Optional[Dict[str, Any]]: - """获取脚本信息""" - script_class = self.loader.script_classes.get(script_name) - if script_class is None: - return None - - return { - "name": script_name, - "class": script_class, - "doc": script_class.__doc__, - "file": inspect.getfile(script_class) if hasattr(script_class, '__file__') else None, - "methods": [method for method in dir(script_class) if not method.startswith('_')] - } - - def reload_script(self, script_name: str) -> bool: - """重新加载脚本""" - script_info = self.get_script_info(script_name) - if script_info and script_info["file"]: - return self.loader.reload_script(script_info["file"]) is not None - return False - - # ==================== 调试功能 ==================== - - def list_all_scripts(self): - """列出所有脚本信息""" - print("\n=== 脚本系统状态 ===") - print(f"可用脚本数量: {len(self.loader.script_classes)}") - print(f"运行中的脚本组件数量: {len(self.engine.script_components)}") - print(f"有脚本的对象数量: {len(self.object_scripts)}") - - if self.loader.script_classes: - print("\n可用脚本:") - for script_name in self.loader.script_classes: - print(f" - {script_name}") - - if self.object_scripts: - print("\n对象脚本分布:") - for obj, scripts in self.object_scripts.items(): - script_names = [s.script_instance.__class__.__name__ for s in scripts] - print(f" - {obj.getName()}: {script_names}") - - print("==================\n") - - def save_object_scripts(self,game_object,node_data:dict): - try: - if game_object in self.object_scripts: - scripts_data = [] - for script_commponent in self.object_scripts[game_object]: - script_info = { - 'script_name':script_commponent.script_name, - 'enabled':script_commponent.enabled, - 'script_class':script_commponent.script_instance.__class__.__name__ - } - scripts_data.append(script_info) - if scripts_data: - node_data['scripts'] = scripts_data - print(f"✓ 保存了 {len(scripts_data)} 个脚本到对象 {game_object.getName()}") - except Exception as e: - print(f"保存对象脚本信息失败: {e}") - traceback.print_exc() - - # def restore_object_scripts(self,game_object,node_data:dict): - # try: - # if 'scripts' in node_data: - # scripts_data = node_data['scripts'] - # restored_count = 0 - # for script_info in scripts_data: - # script_name = script_info.get('script_name') - # enabled = script_info.get('enabled',True) - # - # #检查脚本是否可用 - # if script_name in self.loader.script_classes: - #为 - -# 添加全局便捷函数,让脚本更容易使用API -def get_script_api(): - """获取脚本API实例(需要在脚本管理器初始化后使用)""" - # 这个函数将在脚本系统集成到主系统后实现 - return None - - -# 导出主要类 -__all__ = [ - 'ScriptBase', 'ScriptComponent', 'ScriptEngine', - 'ScriptLoader', 'ScriptAPI', 'ScriptManager' +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +脚本系统模块 + +负责脚本的创建、管理、挂载和运行: +- 脚本管理器:统一管理所有脚本 +- 脚本组件:挂载到游戏对象的脚本实例 +- 脚本引擎:执行脚本逻辑 +- 脚本API:提供给脚本使用的API接口 +""" + +import os +import sys +import importlib +import importlib.util +import traceback +import inspect +import time +from typing import Dict, List, Any, Optional, Callable +from abc import ABC, abstractmethod +from direct.task.TaskManagerGlobal import taskMgr +from panda3d.core import PythonTask + + +class ScriptBase(ABC): + """脚本基类 - 所有用户脚本都应该继承此类""" + + def __init__(self): + self.enabled = True + self.transform = None # 挂载的对象的transform + self.gameObject = None # 挂载的游戏对象 + self.world = None # 引擎世界对象引用 + self._script_id = None + + @abstractmethod + def start(self): + """脚本开始时调用(类似Unity的Start)""" + pass + + @abstractmethod + def update(self, dt): + """每帧更新时调用(类似Unity的Update)""" + pass + + def on_destroy(self): + """脚本销毁时调用(类似Unity的OnDestroy)""" + pass + + def on_enable(self): + """脚本启用时调用""" + pass + + def on_disable(self): + """脚本禁用时调用""" + pass + + def on_collision_enter(self, other): + """碰撞开始时调用""" + pass + + def on_collision_exit(self, other): + """碰撞结束时调用""" + pass + + def log(self, message): + """日志输出""" + print(f"[{self.__class__.__name__}] {message}") + + +class ScriptComponent: + """脚本组件 - 挂载到游戏对象上的脚本实例""" + + def __init__(self, script_instance: ScriptBase, game_object, script_manager): + self.script_instance = script_instance + self.game_object = game_object + self.script_manager = script_manager + self.enabled = True + + # 保存脚本名称,便于UI显示 + self.script_name = script_instance.__class__.__name__ + + # 设置脚本实例的引用 + script_instance.gameObject = game_object + script_instance.transform = game_object # Panda3D中NodePath就是transform + script_instance.world = script_manager.world + script_instance._script_id = id(self) + + # 标记脚本已开始 + self._started = False + + def start(self): + """启动脚本""" + if not self._started and self.enabled: + try: + self.script_instance.start() + self._started = True + except Exception as e: + print(f"脚本启动失败: {e}") + traceback.print_exc() + + def update(self, dt): + """更新脚本""" + if self.enabled and self._started: + try: + self.script_instance.update(dt) + except Exception as e: + print(f"脚本更新失败: {e}") + traceback.print_exc() + + def destroy(self): + """销毁脚本""" + try: + self.script_instance.on_destroy() + except Exception as e: + print(f"脚本销毁失败: {e}") + traceback.print_exc() + + def set_enabled(self, enabled): + """设置脚本启用状态""" + if self.enabled != enabled: + self.enabled = enabled + try: + if enabled: + self.script_instance.on_enable() + else: + self.script_instance.on_disable() + except Exception as e: + print(f"设置脚本状态失败: {e}") + traceback.print_exc() + + +class ScriptEngine: + """脚本引擎 - 负责脚本的执行和生命周期管理""" + + def __init__(self, world): + self.world = world + self.script_components: List[ScriptComponent] = [] + self.update_task = None + + def start_engine(self): + """启动脚本引擎""" + if self.update_task is None: + self.update_task = taskMgr.add(self._update_scripts, "script_update") + print("✓ 脚本引擎已启动") + + def stop_engine(self): + """停止脚本引擎""" + if self.update_task: + taskMgr.remove(self.update_task) + self.update_task = None + print("✓ 脚本引擎已停止") + + def add_script_component(self, script_component: ScriptComponent): + """添加脚本组件""" + self.script_components.append(script_component) + # 如果引擎已运行,立即启动脚本 + if self.update_task: + script_component.start() + + def remove_script_component(self, script_component: ScriptComponent): + """移除脚本组件""" + if script_component in self.script_components: + script_component.destroy() + self.script_components.remove(script_component) + + def _update_scripts(self, task): + """更新所有脚本(每帧调用)""" + from direct.showbase.ShowBaseGlobal import globalClock + dt = globalClock.getDt() + + # 复制列表以避免迭代时修改 + components_copy = self.script_components.copy() + + for component in components_copy: + if component.enabled: + # 如果脚本还没开始,先调用start + if not component._started: + component.start() + # 然后调用update + component.update(dt) + + return task.cont + + +class ScriptLoader: + """脚本加载器 - 负责加载和重载脚本""" + + def __init__(self, script_manager): + self.script_manager = script_manager + self.loaded_modules = {} # 模块名 -> 模块对象 + self.script_classes = {} # 脚本名 -> 脚本类 + self.file_mtimes = {} # 文件路径 -> 修改时间 + + def load_script_from_file(self, script_path: str) -> Optional[type]: + """从文件加载脚本类""" + try: + if not os.path.exists(script_path): + print(f"脚本文件不存在: {script_path}") + return None + + # 获取脚本名称(不包含扩展名) + script_name = os.path.splitext(os.path.basename(script_path))[0] + + # 动态导入模块 + spec = importlib.util.spec_from_file_location(script_name, script_path) + if spec is None: + print(f"无法创建模块规范: {script_path}") + return None + + module = importlib.util.module_from_spec(spec) + + # 如果模块已经加载过,先卸载 + if script_name in self.loaded_modules: + self.unload_script(script_name) + + # 执行模块 + spec.loader.exec_module(module) + + # 查找继承自ScriptBase的类 + script_class = None + for name, obj in inspect.getmembers(module, inspect.isclass): + if issubclass(obj, ScriptBase) and obj != ScriptBase: + script_class = obj + break + + if script_class is None: + print(f"脚本文件中没有找到继承自ScriptBase的类: {script_path}") + return None + + # 保存模块和类信息 + self.loaded_modules[script_name] = module + self.script_classes[script_name] = script_class + self.file_mtimes[script_path] = os.path.getmtime(script_path) + + print(f"✓ 成功加载脚本: {script_name} 从 {script_path}") + return script_class + + except Exception as e: + print(f"加载脚本失败 {script_path}: {e}") + traceback.print_exc() + return None + + def unload_script(self, script_name: str): + """卸载脚本""" + if script_name in self.loaded_modules: + # 移除所有使用此脚本的组件 + components_to_remove = [] + for component in self.script_manager.engine.script_components: + if component.script_instance.__class__.__name__ == script_name: + components_to_remove.append(component) + + for component in components_to_remove: + self.script_manager.remove_script_from_object(component.game_object, script_name) + + # 从sys.modules中移除 + module = self.loaded_modules[script_name] + if module.__name__ in sys.modules: + del sys.modules[module.__name__] + + # 清理引用 + del self.loaded_modules[script_name] + if script_name in self.script_classes: + del self.script_classes[script_name] + + print(f"✓ 脚本已卸载: {script_name}") + + def reload_script(self, script_path: str) -> Optional[type]: + """重新加载脚本(热重载)""" + script_name = os.path.splitext(os.path.basename(script_path))[0] + print(f"重新加载脚本: {script_name}") + + # 先卸载旧版本 + if script_name in self.loaded_modules: + self.unload_script(script_name) + + # 重新加载 + return self.load_script_from_file(script_path) + + def check_for_changes(self): + """检查脚本文件是否有变化(用于热重载)""" + changed_scripts = [] + + for script_path, old_mtime in self.file_mtimes.items(): + if os.path.exists(script_path): + current_mtime = os.path.getmtime(script_path) + if current_mtime > old_mtime: + changed_scripts.append(script_path) + + # 重新加载变化的脚本 + for script_path in changed_scripts: + self.reload_script(script_path) + + return len(changed_scripts) > 0 + + def find_script_file(self, script_name: str) -> Optional[str]: + """根据脚本名称查找脚本文件路径""" + # 首先检查已加载的脚本 + if script_name in self.loaded_modules: + module = self.loaded_modules[script_name] + if hasattr(module, '__file__') and module.__file__: + return module.__file__ + + # 在已知的文件路径中查找 + for file_path in self.file_mtimes.keys(): + file_name = os.path.splitext(os.path.basename(file_path))[0] + if file_name == script_name: + return file_path + + # 在脚本目录中查找 + scripts_dir = self.script_manager.scripts_directory + if os.path.exists(scripts_dir): + for file_name in os.listdir(scripts_dir): + if file_name.endswith('.py'): + base_name = os.path.splitext(file_name)[0] + if base_name == script_name: + return os.path.join(scripts_dir, file_name) + + return None + + +class ScriptAPI: + """脚本API - 提供给脚本使用的API接口""" + + def __init__(self, world): + self.world = world + + # ==================== 游戏对象操作 ==================== + + def find_object_by_name(self, name: str): + """根据名称查找游戏对象""" + return self.world.render.find(name) + + def create_object(self, name: str = "GameObject"): + """创建游戏对象""" + obj = self.world.render.attachNewNode(name) + return obj + + def destroy_object(self, obj): + """销毁游戏对象""" + if obj: + obj.removeNode() + + # ==================== 变换操作 ==================== + + def get_position(self, obj): + """获取对象位置""" + return obj.getPos() if obj else None + + def set_position(self, obj, x, y, z): + """设置对象位置""" + if obj: + obj.setPos(x, y, z) + + def get_rotation(self, obj): + """获取对象旋转""" + return obj.getHpr() if obj else None + + def set_rotation(self, obj, h, p, r): + """设置对象旋转""" + if obj: + obj.setHpr(h, p, r) + + def get_scale(self, obj): + """获取对象缩放""" + return obj.getScale() if obj else None + + def set_scale(self, obj, sx, sy, sz): + """设置对象缩放""" + if obj: + obj.setScale(sx, sy, sz) + + # ==================== 输入系统 ==================== + + def is_key_pressed(self, key): + """检查按键是否被按下""" + # 这里需要集成到现有的输入系统 + return False # 暂时返回False + + # ==================== 时间系统 ==================== + + def get_time(self): + """获取游戏时间""" + return time.time() + + def get_delta_time(self): + """获取帧间隔时间""" + from direct.showbase.ShowBaseGlobal import globalClock + return globalClock.getDt() + + # ==================== 日志系统 ==================== + + def log(self, message): + """输出日志""" + print(f"[ScriptAPI] {message}") + + +class ScriptManager: + """脚本管理器 - 统一管理所有脚本功能""" + + def __init__(self, world): + """初始化脚本管理器 + + Args: + world: 主程序world对象引用 + """ + self.world = world + + # 初始化子系统 + self.engine = ScriptEngine(world) + self.loader = ScriptLoader(self) + self.api = ScriptAPI(world) + + # 脚本存储 + self.object_scripts: Dict[Any, List[ScriptComponent]] = {} # 对象 -> 脚本组件列表 + self.script_templates: Dict[str, type] = {} # 脚本名 -> 脚本类 + + # 脚本目录 + self.scripts_directory = "scripts" + self._ensure_scripts_directory() + + # 热重载监控 + self.hot_reload_enabled = True + self.hot_reload_task = None + + print("✓ 脚本管理系统初始化完成") + + def _ensure_scripts_directory(self): + """确保脚本目录存在""" + if not os.path.exists(self.scripts_directory): + os.makedirs(self.scripts_directory) + + # 创建示例脚本 + self._create_example_script() + + def _create_example_script(self): + """创建示例脚本""" + example_script = '''#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +示例脚本 - 演示如何编写脚本 +""" + +from core.script_system import ScriptBase + +class ExampleScript(ScriptBase): + """示例脚本类""" + + def __init__(self): + super().__init__() + self.counter = 0 + self.rotation_speed = 30.0 # 度/秒 + + def start(self): + """脚本开始时调用""" + self.log("示例脚本开始运行!") + self.log(f"挂载到对象: {self.gameObject.getName()}") + + def update(self, dt): + """每帧更新""" + self.counter += 1 + + # 每60帧输出一次信息 + if self.counter % 60 == 0: + self.log(f"运行了 {self.counter} 帧") + + # 让对象旋转 + if self.transform: + current_h = self.transform.getH() + new_h = current_h + self.rotation_speed * dt + self.transform.setH(new_h) + + def on_destroy(self): + """脚本销毁时调用""" + self.log("示例脚本被销毁") + + def on_enable(self): + """脚本启用时调用""" + self.log("示例脚本被启用") + + def on_disable(self): + """脚本禁用时调用""" + self.log("示例脚本被禁用") +''' + + example_path = os.path.join(self.scripts_directory, "example_script.py") + with open(example_path, 'w', encoding='utf-8') as f: + f.write(example_script) + + print(f"✓ 创建示例脚本: {example_path}") + + # ==================== 脚本管理功能 ==================== + + def start_system(self): + """启动脚本系统""" + self.engine.start_engine() + + # 加载scripts目录中的所有现有脚本 + self.load_all_scripts_from_directory() + + if self.hot_reload_enabled: + self.start_hot_reload() + + print("✓ 脚本系统已启动") + + def stop_system(self): + """停止脚本系统""" + self.engine.stop_engine() + self.stop_hot_reload() + print("✓ 脚本系统已停止") + + def start_hot_reload(self): + """启动热重载监控""" + if self.hot_reload_task is None: + self.hot_reload_task = taskMgr.add(self._check_hot_reload, "script_hot_reload") + print("✓ 脚本热重载监控已启动") + + def stop_hot_reload(self): + """停止热重载监控""" + if self.hot_reload_task: + taskMgr.remove(self.hot_reload_task) + self.hot_reload_task = None + print("✓ 脚本热重载监控已停止") + + def _check_hot_reload(self, task): + """检查热重载(每秒调用一次)""" + self.loader.check_for_changes() + task.delayTime = 1.0 # 1秒后再次调用 + return task.again + + # ==================== 脚本创建和加载 ==================== + + def create_script_file(self, script_name: str, template: str = "basic") -> str: + """创建新的脚本文件""" + script_path = os.path.join(self.scripts_directory, f"{script_name}.py") + + if os.path.exists(script_path): + print(f"脚本文件已存在: {script_path}") + return script_path + + # 根据模板创建脚本 + if template == "basic": + script_content = self._get_basic_script_template(script_name) + elif template == "movement": + script_content = self._get_movement_script_template(script_name) + else: + script_content = self._get_basic_script_template(script_name) + + with open(script_path, 'w', encoding='utf-8') as f: + f.write(script_content) + + print(f"✓ 创建脚本文件: {script_path}") + return script_path + + def _get_basic_script_template(self, script_name: str) -> str: + """获取基础脚本模板""" + class_name = ''.join(word.capitalize() for word in script_name.split('_')) + + return f'''#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +{script_name} - 自定义脚本 +""" + +from core.script_system import ScriptBase + +class {class_name}(ScriptBase): + """自定义脚本类""" + + def __init__(self): + super().__init__() + # 在这里初始化您的变量 + + def start(self): + """脚本开始时调用""" + self.log("脚本开始运行!") + + def update(self, dt): + """每帧更新""" + # 在这里编写更新逻辑 + pass + + def on_destroy(self): + """脚本销毁时调用""" + self.log("脚本被销毁") +''' + + def _get_movement_script_template(self, script_name: str) -> str: + """获取移动脚本模板""" + class_name = ''.join(word.capitalize() for word in script_name.split('_')) + + return f'''#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +{script_name} - 移动脚本 +""" + +from core.script_system import ScriptBase + +class {class_name}(ScriptBase): + """移动脚本类""" + + def __init__(self): + super().__init__() + self.speed = 5.0 # 移动速度 + self.direction = [1, 0, 0] # 移动方向 + + def start(self): + """脚本开始时调用""" + self.log("移动脚本开始运行!") + + def update(self, dt): + """每帧更新""" + if self.transform: + # 计算移动偏移 + offset_x = self.direction[0] * self.speed * dt + offset_y = self.direction[1] * self.speed * dt + offset_z = self.direction[2] * self.speed * dt + + # 更新位置 + current_pos = self.transform.getPos() + new_pos = ( + current_pos.x + offset_x, + current_pos.y + offset_y, + current_pos.z + offset_z + ) + self.transform.setPos(*new_pos) + + def on_destroy(self): + """脚本销毁时调用""" + self.log("移动脚本被销毁") +''' + + def load_script_from_file(self, script_path: str) -> Optional[type]: + """从文件加载脚本""" + return self.loader.load_script_from_file(script_path) + + def load_all_scripts_from_directory(self, directory: str = None) -> List[str]: + """从目录加载所有脚本""" + if directory is None: + directory = self.scripts_directory + + if not os.path.exists(directory): + print(f"脚本目录不存在: {directory}") + return [] + + loaded_scripts = [] + for filename in os.listdir(directory): + if filename.endswith('.py') and not filename.startswith('__'): + script_path = os.path.join(directory, filename) + script_class = self.load_script_from_file(script_path) + if script_class: + script_name = os.path.splitext(filename)[0] + loaded_scripts.append(script_name) + + print(f"✓ 从目录 {directory} 加载了 {len(loaded_scripts)} 个脚本") + return loaded_scripts + + # ==================== 脚本挂载和管理 ==================== + + def add_script_to_object(self, game_object, script_name: str) -> Optional[ScriptComponent]: + """为对象添加脚本""" + # 查找脚本类 + script_class = self.loader.script_classes.get(script_name) + if script_class is None: + print(f"未找到脚本类: {script_name}") + return None + + try: + # 创建脚本实例 + script_instance = script_class() + + # 创建脚本组件 + script_component = ScriptComponent(script_instance, game_object, self) + + # 添加到对象的脚本列表 + if game_object not in self.object_scripts: + self.object_scripts[game_object] = [] + self.object_scripts[game_object].append(script_component) + + # 添加到脚本引擎 + self.engine.add_script_component(script_component) + + print(f"✓ 为对象 {game_object.getName()} 添加脚本: {script_name}") + return script_component + + except Exception as e: + print(f"添加脚本失败: {e}") + traceback.print_exc() + return None + + def remove_script_from_object(self, game_object, script_name: str) -> bool: + """从游戏对象移除脚本""" + if game_object not in self.object_scripts: + return False + + script_components = self.object_scripts[game_object] + removed = False + + for component in script_components[:]: # 复制列表以避免修改时出错 + if component.script_instance.__class__.__name__ == script_name: + # 从引擎移除 + self.engine.remove_script_component(component) + # 从对象脚本列表移除 + script_components.remove(component) + removed = True + + print(f"✓ 从对象 {game_object.getName()} 移除脚本: {script_name}") + + + if not script_components: + del self.object_scripts[game_object] + + # 更新节点上保存的脚本信息标签 + if removed: + self._update_node_script_tags_after_removal(game_object, script_name) + + return removed + + def _update_node_script_tags_after_removal(self, game_object, removed_script_name): + """在移除脚本后更新节点标签""" + try: + # 获取对象上剩余的脚本 + remaining_scripts = self.get_scripts_on_object(game_object) + + if not remaining_scripts: + # 如果没有其他脚本,清除所有脚本标签 + if game_object.hasTag("has_scripts"): + game_object.clearTag("has_scripts") + if game_object.hasTag("scripts_info"): + game_object.clearTag("scripts_info") + print(f"✓ 清除节点 {game_object.getName()} 的所有脚本标签") + else: + # 如果还有其他脚本,更新脚本信息标签 + script_info_list = [] + for script_component in remaining_scripts: + script_name = script_component.script_name + script_class = script_component.script_instance.__class__ + script_file = self.loader.find_script_file(script_name) or "" + + script_info_list.append({ + "name": script_name, + "file": script_file + }) + + import json + game_object.setTag("has_scripts", "true") + game_object.setTag("scripts_info", json.dumps(script_info_list, ensure_ascii=False)) + print(f"✓ 更新节点 {game_object.getName()} 的脚本标签信息,剩余 {len(script_info_list)} 个脚本") + + except Exception as e: + print(f"更新节点标签失败: {e}") + + def get_scripts_on_object(self, game_object) -> List[ScriptComponent]: + """获取对象上的所有脚本""" + return self.object_scripts.get(game_object, []) + + def get_script_on_object(self, game_object, script_name: str) -> Optional[ScriptComponent]: + """获取对象上的特定脚本""" + scripts = self.get_scripts_on_object(game_object) + for script in scripts: + if script.script_instance.__class__.__name__ == script_name: + return script + return None + + # ==================== 脚本信息查询 ==================== + + def get_available_scripts(self) -> List[str]: + """获取所有可用的脚本名称""" + return list(self.loader.script_classes.keys()) + + def get_script_info(self, script_name: str) -> Optional[Dict[str, Any]]: + """获取脚本信息""" + script_class = self.loader.script_classes.get(script_name) + if script_class is None: + return None + + return { + "name": script_name, + "class": script_class, + "doc": script_class.__doc__, + "file": inspect.getfile(script_class) if hasattr(script_class, '__file__') else None, + "methods": [method for method in dir(script_class) if not method.startswith('_')] + } + + def reload_script(self, script_name: str) -> bool: + """重新加载脚本""" + script_info = self.get_script_info(script_name) + if script_info and script_info["file"]: + return self.loader.reload_script(script_info["file"]) is not None + return False + + # ==================== 调试功能 ==================== + + def list_all_scripts(self): + """列出所有脚本信息""" + print("\n=== 脚本系统状态 ===") + print(f"可用脚本数量: {len(self.loader.script_classes)}") + print(f"运行中的脚本组件数量: {len(self.engine.script_components)}") + print(f"有脚本的对象数量: {len(self.object_scripts)}") + + if self.loader.script_classes: + print("\n可用脚本:") + for script_name in self.loader.script_classes: + print(f" - {script_name}") + + if self.object_scripts: + print("\n对象脚本分布:") + for obj, scripts in self.object_scripts.items(): + script_names = [s.script_instance.__class__.__name__ for s in scripts] + print(f" - {obj.getName()}: {script_names}") + + print("==================\n") + + def save_object_scripts(self,game_object,node_data:dict): + try: + if game_object in self.object_scripts: + scripts_data = [] + for script_commponent in self.object_scripts[game_object]: + script_info = { + 'script_name':script_commponent.script_name, + 'enabled':script_commponent.enabled, + 'script_class':script_commponent.script_instance.__class__.__name__ + } + scripts_data.append(script_info) + if scripts_data: + node_data['scripts'] = scripts_data + print(f"✓ 保存了 {len(scripts_data)} 个脚本到对象 {game_object.getName()}") + except Exception as e: + print(f"保存对象脚本信息失败: {e}") + traceback.print_exc() + + # def restore_object_scripts(self,game_object,node_data:dict): + # try: + # if 'scripts' in node_data: + # scripts_data = node_data['scripts'] + # restored_count = 0 + # for script_info in scripts_data: + # script_name = script_info.get('script_name') + # enabled = script_info.get('enabled',True) + # + # #检查脚本是否可用 + # if script_name in self.loader.script_classes: + #为 + +# 添加全局便捷函数,让脚本更容易使用API +def get_script_api(): + """获取脚本API实例(需要在脚本管理器初始化后使用)""" + # 这个函数将在脚本系统集成到主系统后实现 + return None + + +# 导出主要类 +__all__ = [ + 'ScriptBase', 'ScriptComponent', 'ScriptEngine', + 'ScriptLoader', 'ScriptAPI', 'ScriptManager' ] \ No newline at end of file diff --git a/demo.py b/demo.py index 2fd825ca..d2fb0dd1 100644 --- a/demo.py +++ b/demo.py @@ -257,6 +257,12 @@ class MyWorld(CoreWorld): self.showToolbar = True self.showResourceManager = True + # 脚本系统状态变量 + self.hotReloadEnabled = True + self._new_script_name = "new_script" + self._selected_template = 0 + self._mount_script_index = 0 + # 变换监控相关 self._transform_monitoring = False self._monitored_node = None @@ -4390,44 +4396,380 @@ class MyWorld(CoreWorld): self.add_info_message("相机位置已重置") def _draw_script_panel(self): - """绘制脚本管理面板""" + """绘制脚本管理面板(与Qt版本功能一致)""" # 使用面板类型的窗口标志,支持docking flags = self.style_manager.get_window_flags("panel") with self.style_manager.begin_styled_window("脚本管理", self.showScriptPanel, flags): self.showScriptPanel = True # 确保窗口保持打开 - imgui.text("脚本列表") - imgui.separator() + # 1. 脚本系统状态组 + self._draw_script_status_group() - # 模拟脚本列表 - selected, _ = imgui.selectable("main.py", False) - if selected: - print("选择脚本: main.py") - selected, _ = imgui.selectable("player_controller.py", False) - if selected: - print("选择脚本: player_controller.py") - selected, _ = imgui.selectable("ui_manager.py", False) - if selected: - print("选择脚本: ui_manager.py") + imgui.spacing() - imgui.separator() + # 2. 创建脚本组 + self._draw_create_script_group() - # 脚本操作按钮 - if imgui.button("新建脚本"): - print("新建脚本") + imgui.spacing() + + # 3. 可用脚本组 + self._draw_available_scripts_group() + + imgui.spacing() + + # 4. 脚本挂载组 + self._draw_script_mounting_group() + + def _draw_script_status_group(self): + """绘制脚本系统状态组""" + if imgui.collapsing_header("脚本系统状态", imgui.TreeNodeFlags_.default_open): + # 脚本系统状态 + imgui.text("脚本引擎状态:") imgui.same_line() - if imgui.button("编辑"): - print("编辑脚本") + + # 检查脚本管理器是否正常工作 + if hasattr(self, 'script_manager') and self.script_manager: + if hasattr(self.script_manager, 'engine') and self.script_manager.engine: + imgui.text_colored((0.0, 1.0, 0.0, 1.0), "✓ 已启动") + else: + imgui.text_colored((1.0, 0.5, 0.0, 1.0), "⚠ 引擎未初始化") + else: + imgui.text_colored((1.0, 0.0, 0.0, 1.0), "✗ 未启动") + + # 热重载状态 + imgui.text("热重载状态:") imgui.same_line() - if imgui.button("重载"): - print("重载脚本") + + hot_reload_enabled = False + if hasattr(self, 'script_manager') and self.script_manager: + hot_reload_enabled = getattr(self.script_manager, 'hot_reload_enabled', False) + + if hot_reload_enabled: + imgui.text_colored((0.0, 1.0, 0.0, 1.0), "✓ 已启用") + else: + imgui.text_colored((1.0, 0.5, 0.0, 1.0), "✗ 已禁用") + + imgui.same_line() + if imgui.button("切换热重载##toggle_hot_reload"): + self._toggle_hot_reload() + + def _draw_create_script_group(self): + """绘制创建脚本组""" + if imgui.collapsing_header("创建脚本"): + # 脚本名称输入 + imgui.text("脚本名称:") + imgui.same_line() + + # 获取当前脚本名称 + if not hasattr(self, '_new_script_name'): + self._new_script_name = "new_script" + + changed, new_name = imgui.input_text("##script_name", self._new_script_name, 256) + if changed: + self._new_script_name = new_name + + # 模板选择 + imgui.text("脚本模板:") + imgui.same_line() + + if not hasattr(self, '_selected_template'): + self._selected_template = 0 + + templates = ["基础脚本", "移动脚本", "旋转脚本", "缩放脚本", "动画脚本"] + changed, selected = imgui.combo("##script_template", self._selected_template, templates) + if changed: + self._selected_template = selected + + # 创建按钮 + if imgui.button("创建脚本##create_script"): + self._create_new_script() + + imgui.same_line() + if imgui.button("从文件创建##create_from_file"): + self._on_create_script() + + def _draw_available_scripts_group(self): + """绘制可用脚本组""" + if imgui.collapsing_header("可用脚本"): + # 刷新脚本列表 + if imgui.button("刷新列表##refresh_scripts"): + self._refresh_scripts_list() + + imgui.same_line() + if imgui.button("重载全部##reload_all_scripts"): + self._reload_all_scripts() imgui.separator() - imgui.text("脚本输出:") - imgui.text("脚本引擎已启动") - imgui.text("热重载监控已启动") + # 获取可用脚本列表 + available_scripts = [] + if hasattr(self, 'script_manager') and self.script_manager: + try: + available_scripts = self.script_manager.get_available_scripts() + except Exception as e: + print(f"获取脚本列表失败: {e}") + + # 显示脚本列表 + if available_scripts: + for i, script_name in enumerate(available_scripts): + selected, _ = imgui.selectable(f"{script_name}##script_{i}", False) + if selected: + self._on_script_selected(script_name) + + # 双击编辑 + if imgui.is_item_hovered() and imgui.is_mouse_double_clicked(0): + self._edit_script(script_name) + else: + imgui.text_colored((0.7, 0.7, 0.7, 1.0), "无可用脚本") + + def _draw_script_mounting_group(self): + """绘制脚本挂载组""" + if imgui.collapsing_header("脚本挂载"): + # 显示当前选中对象 + selected_node = None + if hasattr(self, 'selection') and self.selection and hasattr(self.selection, 'selectedNode'): + selected_node = self.selection.selectedNode + + if selected_node and not selected_node.isEmpty(): + imgui.text("选中对象:") + imgui.same_line() + imgui.text_colored((0.0, 1.0, 0.0, 1.0), selected_node.getName() or "未命名对象") + + imgui.spacing() + + # 脚本选择和挂载 + imgui.text("选择脚本:") + imgui.same_line() + + # 获取可用脚本 + available_scripts = [] + if hasattr(self, 'script_manager') and self.script_manager: + try: + available_scripts = self.script_manager.get_available_scripts() + except Exception as e: + print(f"获取脚本列表失败: {e}") + + if available_scripts: + if not hasattr(self, '_mount_script_index'): + self._mount_script_index = 0 + + changed, selected = imgui.combo("##mount_script", self._mount_script_index, available_scripts) + if changed: + self._mount_script_index = selected + + imgui.same_line() + if imgui.button("挂载##mount_script"): + if self._mount_script_index < len(available_scripts): + script_name = available_scripts[self._mount_script_index] + self._mount_script_to_selected(script_name) + else: + imgui.text_colored((0.7, 0.7, 0.7, 1.0), "无可用脚本") + + imgui.spacing() + + # 显示已挂载的脚本 + imgui.text("已挂载脚本:") + + mounted_scripts = [] + if hasattr(self, 'script_manager') and self.script_manager: + try: + mounted_scripts = self.script_manager.get_scripts_on_object(selected_node) + except Exception as e: + print(f"获取已挂载脚本失败: {e}") + + if mounted_scripts: + for i, script_component in enumerate(mounted_scripts): + # 从ScriptComponent获取脚本名称 + script_name = getattr(script_component, 'script_name', None) + if not script_name and hasattr(script_component, '__class__'): + script_name = script_component.__class__.__name__ + + if not script_name: + script_name = f"Script_{i}" + + imgui.text(f"• {script_name}") + imgui.same_line() + if imgui.button(f"卸载##unmount_{i}"): + self._unmount_script_from_selected(script_name) + imgui.same_line() + if imgui.button(f"编辑##edit_mounted_{i}"): + self._edit_script(script_name) + else: + imgui.text_colored((0.7, 0.7, 0.7, 1.0), "无挂载脚本") + else: + imgui.text_colored((0.7, 0.7, 0.7, 1.0), "请先选择一个对象") + + def _toggle_hot_reload(self): + """切换热重载状态""" + if hasattr(self, 'script_manager') and self.script_manager: + try: + current_state = getattr(self.script_manager, 'hot_reload_enabled', False) + self.script_manager.hot_reload_enabled = not current_state + + new_state = "启用" if not current_state else "禁用" + self.add_success_message(f"热重载已{new_state}") + print(f"[脚本系统] 热重载已{new_state}") + except Exception as e: + self.add_error_message(f"切换热重载失败: {str(e)}") + print(f"[脚本系统] 切换热重载失败: {e}") + + def _create_new_script(self): + """创建新脚本""" + if not hasattr(self, '_new_script_name') or not self._new_script_name.strip(): + self.add_error_message("请输入脚本名称") + return + + script_name = self._new_script_name.strip() + if not script_name.endswith('.py'): + script_name += '.py' + + # 确定模板类型 + template_map = { + 0: "basic", + 1: "movement", + 2: "rotation", + 3: "scale", + 4: "animation" + } + template_type = template_map.get(getattr(self, '_selected_template', 0), "basic") + + try: + if hasattr(self, 'script_manager') and self.script_manager: + result = self.script_manager.create_script_file(script_name, template_type) + if result: + self.add_success_message(f"脚本 {script_name} 创建成功") + print(f"[脚本系统] 创建脚本成功: {script_name}") + # 刷新脚本列表 + self._refresh_scripts_list() + else: + self.add_error_message(f"脚本 {script_name} 创建失败") + else: + self.add_error_message("脚本管理器未初始化") + except Exception as e: + self.add_error_message(f"创建脚本失败: {str(e)}") + print(f"[脚本系统] 创建脚本失败: {e}") + + def _refresh_scripts_list(self): + """刷新脚本列表""" + try: + if hasattr(self, 'script_manager') and self.script_manager: + # 这里可以添加缓存逻辑,避免频繁刷新 + available_scripts = self.script_manager.get_available_scripts() + print(f"[脚本系统] 刷新脚本列表: {len(available_scripts)} 个脚本") + self.add_success_message(f"脚本列表已刷新,共 {len(available_scripts)} 个脚本") + else: + self.add_error_message("脚本管理器未初始化") + except Exception as e: + self.add_error_message(f"刷新脚本列表失败: {str(e)}") + print(f"[脚本系统] 刷新脚本列表失败: {e}") + + def _reload_all_scripts(self): + """重载所有脚本""" + try: + if hasattr(self, 'script_manager') and self.script_manager: + # 获取所有可用脚本并逐个重载 + available_scripts = self.script_manager.get_available_scripts() + success_count = 0 + + for script_name in available_scripts: + if self.script_manager.reload_script(script_name): + success_count += 1 + + self.add_success_message(f"重载完成: {success_count}/{len(available_scripts)} 个脚本成功") + print(f"[脚本系统] 重载脚本: {success_count}/{len(available_scripts)} 成功") + else: + self.add_error_message("脚本管理器未初始化") + except Exception as e: + self.add_error_message(f"重载脚本失败: {str(e)}") + print(f"[脚本系统] 重载脚本失败: {e}") + + def _on_script_selected(self, script_name): + """处理脚本选择事件""" + print(f"[脚本系统] 选择脚本: {script_name}") + self.add_info_message(f"已选择脚本: {script_name}") + + def _edit_script(self, script_name): + """编辑脚本""" + try: + if hasattr(self, 'script_manager') and self.script_manager: + # 获取脚本信息 + script_info = self.script_manager.get_script_info(script_name) + if script_info and script_info.get("file"): + script_path = script_info["file"] + + # 打开系统默认编辑器 + import subprocess + import platform + + system = platform.system() + try: + if system == "Windows": + subprocess.run(['notepad', script_path]) + elif system == "Darwin": # macOS + subprocess.run(['open', script_path]) + else: # Linux + subprocess.run(['xdg-open', script_path]) + + self.add_success_message(f"已打开脚本编辑器: {script_name}") + print(f"[脚本系统] 编辑脚本: {script_path}") + except Exception as e: + self.add_error_message(f"打开编辑器失败: {str(e)}") + else: + self.add_error_message(f"找不到脚本文件: {script_name}") + else: + self.add_error_message("脚本管理器未初始化") + except Exception as e: + self.add_error_message(f"编辑脚本失败: {str(e)}") + print(f"[脚本系统] 编辑脚本失败: {e}") + + def _mount_script_to_selected(self, script_name): + """挂载脚本到选中对象""" + selected_node = None + if hasattr(self, 'selection') and self.selection and hasattr(self.selection, 'selectedNode'): + selected_node = self.selection.selectedNode + + if not selected_node or selected_node.isEmpty(): + self.add_error_message("请先选择一个对象") + return + + try: + if hasattr(self, 'script_manager') and self.script_manager: + script_component = self.script_manager.add_script_to_object(selected_node, script_name) + if script_component: + self.add_success_message(f"脚本 {script_name} 已挂载到 {selected_node.getName()}") + print(f"[脚本系统] 挂载脚本: {script_name} -> {selected_node.getName()}") + else: + self.add_error_message(f"挂载脚本 {script_name} 失败") + else: + self.add_error_message("脚本管理器未初始化") + except Exception as e: + self.add_error_message(f"挂载脚本失败: {str(e)}") + print(f"[脚本系统] 挂载脚本失败: {e}") + + def _unmount_script_from_selected(self, script_name): + """从选中对象卸载脚本""" + selected_node = None + if hasattr(self, 'selection') and self.selection and hasattr(self.selection, 'selectedNode'): + selected_node = self.selection.selectedNode + + if not selected_node or selected_node.isEmpty(): + self.add_error_message("请先选择一个对象") + return + + try: + if hasattr(self, 'script_manager') and self.script_manager: + result = self.script_manager.remove_script_from_object(selected_node, script_name) + if result: + self.add_success_message(f"脚本 {script_name} 已从 {selected_node.getName()} 卸载") + print(f"[脚本系统] 卸载脚本: {script_name} <- {selected_node.getName()}") + else: + self.add_error_message(f"卸载脚本 {script_name} 失败") + else: + self.add_error_message("脚本管理器未初始化") + except Exception as e: + self.add_error_message(f"卸载脚本失败: {str(e)}") + print(f"[脚本系统] 卸载脚本失败: {e}") # ==================== 菜单处理函数 ====================