<think>我们遇到了一个错误:`LogPython: Error: 重载失败 main: 'main'`。这个错误通常发生在尝试重新加载模块时,但模块中并没有定义`main`函数,而我们的重载逻辑可能期望模块中有这样一个函数。
回顾之前的代码,我们在`toolbar_setup.py`中定义了一个`main`函数,并且在重载时尝试调用它。但是,错误信息表明在重载过程中调用`main`时失败了,提示`'main'`未定义。
可能的原因:
1. 模块中确实没有名为`main`的函数。
2. 模块在重新加载后,函数引用丢失。
在Python中,当我们使用`importlib.reload(module)`重新加载一个模块时,模块的代码会重新执行,但原来对模块的引用(比如模块中的函数)可能不会更新。因此,如果我们在重新加载之前存储了对模块中函数的引用,重新加载后这些引用可能还是指向旧函数(如果模块级代码没有重新定义这些函数,则可能被清除)。
但是,在我们的重载逻辑中,我们是在重新加载模块后,再次从模块中获取`main`函数。如果重新加载后模块中确实没有`main`函数,就会导致这个错误。
让我们检查`toolbar_setup.py`的内容,确保它定义了`main`函数。如果没有,我们需要定义它。另外,也要确保重载逻辑正确。
根据之前的讨论,`toolbar_setup.py`的内容应该类似于:
```python
import unreal
import os
import importlib
# 其他函数定义...
def main():
# 主函数逻辑,比如创建工具栏等
pass
```
如果文件中有`main`函数,那么错误可能是由于重载过程中模块对象的问题。
我们之前重载脚本的代码可能是这样的:
```python
def reload_scripts():
# 重新加载所有脚本
for script in scripts_to_watch:
try:
module = sys.modules.get(script)
if module:
importlib.reload(module)
# 尝试调用main函数
if hasattr(module, 'main'):
module.main()
else:
unreal.log_warning(f"模块 {script} 没有main函数")
else:
unreal.log(f"模块 {script} 尚未导入,跳过重载")
except Exception as e:
unreal.log_error(f"重载 {script} 失败: {str(e)}")
```
但是,在重载后,模块中的函数应该被重新执行,所以`main`函数应该存在。然而,错误信息显示重载失败是因为`main`不存在。
另一种可能是:在第一次运行脚本时,我们调用了`main`函数,但重载时,由于某种原因,模块没有被正确重新加载,或者模块在重新加载时抛出了异常,导致模块没有被正确加载,因此`main`函数不存在。
因此,我们需要在重载时捕获每个模块的异常,并记录错误,然后继续处理其他模块。
但是,错误信息中已经提到“重载失败”,并且具体到`main`函数不存在,所以可能是该模块确实没有定义`main`函数。
解决方案:
1. 检查`toolbar_setup.py`,确保它有一个`main`函数。
2. 在重载逻辑中,如果模块没有`main`函数,则跳过调用,并记录警告(如上例所示),而不是抛出错误导致中断。
修改重载函数,使其更健壮:
```python
def reload_scripts():
scripts = list(scripts_to_watch) # 防止在迭代中修改
for script in scripts:
# 获取模块名(不带扩展名)
module_name = os.path.splitext(script)[0]
if module_name in sys.modules:
try:
module = sys.modules[module_name]
importlib.reload(module)
# 检查是否有main函数
if hasattr(module, 'main'):
module.main()
unreal.log(f"✅ 成功重载并执行 {module_name}")
else:
unreal.log_warning(f"⚠️ 模块 {module_name} 没有'main'函数,已重载但未执行")
except Exception as e:
unreal.log_error(f"重载 {module_name} 失败: {str(e)}")
else:
unreal.log(f"模块 {module_name} 尚未导入,跳过重载")
```
另外,注意在文件系统观察器中触发重载时,我们可能添加了新脚本,所以需要更新`scripts_to_watch`。但我们之前的逻辑是每次文件变化都重新扫描目录,所以`scripts_to_watch`应该已经更新。
现在,根据错误日志,我们看到:
```
LogPython: 🔍 扫描目录: C:\Users\EDY\Desktop\TA工具开发\Plugins\Python\Content\Python
LogPython: 🔄 Python 自动刷新已启用
LogPython: Hello UE Python!
LogPython: Error: 重载失败 main: 'main'
LogPython: ✅ 重载完成! 共处理 0 个脚本
```
注意最后一句“共处理0个脚本”,这意味着在重载时,`scripts_to_watch`可能是空的,所以没有重载任何脚本。那么为什么会出现关于`main`的错误呢?
可能的原因:我们在第一次运行`toolbar_setup.py`时,它导入了其他模块,并把这些模块加入到`scripts_to_watch`中。但在重载时,可能这些模块没有被正确扫描到,或者路径处理有问题。
我们需要确保:
- 扫描目录时能够正确识别所有脚本文件(包括子目录)。
- 将脚本路径转换为模块名(去掉扩展名,并将路径分隔符转换为点,如果是在包内的话)。
由于我们的脚本在`Content/Python`目录下,我们可以将这个目录添加到`sys.path`,然后使用模块名(例如,如果有一个脚本`foo/bar.py`,则模块名为`foo.bar`)来导入。
但是,在文件观察中,我们使用的是文件的绝对路径,而模块名应该是相对于`sys.path`中某个路径的相对路径(去掉扩展名,并将分隔符替换为点)。
因此,我们需要在扫描脚本时,记录每个脚本的模块名。例如:
```python
# 在扫描目录时
base_dir = "C:/Users/EDY/Desktop/TA工具开发/Plugins/Python/Content/Python"
scripts_to_watch = {} # 路径: 模块名
for root, dirs, files in os.walk(base_dir):
for file in files:
if file.endswith('.py'):
full_path = os.path.join(root, file)
# 计算相对路径
rel_path = os.path.relpath(full_path, base_dir)
# 将路径分隔符转换为点,并去掉扩展名
module_name = os.path.splitext(rel_path)[0].replace(os.sep, '.')
# 如果文件就在base_dir下,那么rel_path就是文件名,没有目录,那么module_name就是文件名(不带扩展名)
scripts_to_watch[full_path] = module_name
```
然后,在重载时,我们使用模块名来查找`sys.modules`中的模块。
但是,请注意,在导入模块时,我们可能并不是用这个相对路径作为模块名导入的。因为我们可能已经将`base_dir`添加到`sys.path`,然后使用`import foo.bar`的方式导入。
所以,这样得到的模块名应该是正确的。
然而,在第一次导入时,我们可能需要动态导入这些模块。我们可以使用以下方式:
```python
# 在第一次运行脚本时,导入所有脚本并调用main
for full_path, module_name in scripts_to_watch.items():
if module_name not in sys.modules:
try:
module = importlib.import_module(module_name)
if hasattr(module, 'main'):
module.main()
except Exception as e:
unreal.log_error(f"导入并执行 {module_name} 失败: {str(e)}")
```
这样,我们确保每个脚本都被导入,并且如果定义了`main`函数,就被执行。
现在,让我们整合这些思路,修改我们的自动刷新脚本。
由于代码较长,我将提供一个修改后的版本,重点放在扫描脚本、导入脚本和重载脚本上。
注意:由于我们是在Unreal Engine的Python环境中运行,我们需要小心处理路径和导入。
以下是修改后的自动刷新脚本(假设这个脚本是独立运行的,比如在UE的Python命令行中执行,或者放在启动时运行的脚本中):
由于问题描述中只有一个报错,并且重载了0个脚本,我们首先需要解决为什么`scripts_to_watch`为空的问题。
可能的原因是在扫描目录时,目录路径不正确?我们检查日志中的扫描目录:`C:\Users\EDY\Desktop\TA工具开发\Plugins\Python\Content\Python`,这应该是正确的。
因此,可能是扫描逻辑没有找到任何.py文件?或者我们在扫描后没有正确存储?
我们修改扫描逻辑,并记录扫描到的文件数量。
另外,在重载完成时,我们记录的是处理了0个脚本,说明在重载循环中,`scripts_to_watch`为空。
因此,我们修改扫描函数,确保它更新`scripts_to_watch`字典(包括模块名),并且在重载时使用这个字典。
由于代码较长,我在这里提供一个完整的修改版本,并加上详细注释:
注意:这个脚本将同时负责初始化和自动刷新。
```python
import unreal
import os
import sys
import importlib
import time
from collections import defaultdict
from pathlib import Path
# 基础目录:Content/Python文件夹的绝对路径
BASE_DIR = Path("C:/Users/EDY/Desktop/TA工具开发/Plugins/Python/Content/Python")
# 将基础目录添加到sys.path,以便导入该目录下的模块
sys.path.append(str(BASE_DIR))
# 存储需要监视的脚本:key为脚本的绝对路径,value为模块名
scripts_to_watch = {}
# 存储每个模块的最后修改时间
last_modification_times = defaultdict(float)
def scan_scripts():
"""扫描基础目录下的所有Python脚本,更新scripts_to_watch"""
global scripts_to_watch
new_scripts = {}
unreal.log("🔍 扫描目录: " + str(BASE_DIR))
for root, dirs, files in os.walk(BASE_DIR):
for file in files:
if file.endswith('.py') and not file.startswith('__'):
full_path = Path(root) / file
# 计算相对于基础目录的路径
rel_path = full_path.relative_to(BASE_DIR)
# 将路径转换为模块名:去掉扩展名,将路径分隔符替换为点
module_name = str(rel_path.with_suffix('')).replace(os.sep, '.')
new_scripts[str(full_path)] = module_name
# 更新scripts_to_watch
scripts_to_watch = new_scripts
unreal.log(f"找到 {len(scripts_to_watch)} 个脚本")
def import_and_run_scripts():
"""导入并运行所有脚本的main函数(如果存在)"""
for full_path, module_name in scripts_to_watch.items():
# 记录修改时间
last_modification_times[full_path] = os.path.getmtime(full_path)
if module_name in sys.modules:
# 如果已经导入,则跳过(因为后面会有重载)
continue
try:
module = importlib.import_module(module_name)
# 如果模块有main函数,则执行
if hasattr(module, 'main'):
module.main()
unreal.log(f"执行 {module_name}.main()")
except Exception as e:
unreal.log_error(f"导入并执行 {module_name} 失败: {str(e)}")
def reload_scripts():
"""重新加载所有发生变化的脚本"""
scan_scripts() # 重新扫描,以获取新增脚本
reload_count = 0
error_count = 0
for full_path, module_name in scripts_to_watch.items():
# 检查文件修改时间
current_mtime = os.path.getmtime(full_path)
last_mtime = last_modification_times.get(full_path, 0)
if current_mtime > last_mtime:
# 更新修改时间
last_modification_times[full_path] = current_mtime
# 重载模块
if module_name in sys.modules:
try:
module = sys.modules[module_name]
importlib.reload(module)
if hasattr(module, 'main'):
module.main()
unreal.log(f"✅ 重载并执行 {module_name}")
else:
unreal.log_warning(f"⚠️ 模块 {module_name} 没有'main'函数,已重载但未执行")
reload_count += 1
except Exception as e:
unreal.log_error(f"重载 {module_name} 失败: {str(e)}")
error_count += 1
else:
# 新脚本,尚未导入,尝试导入并运行
try:
module = importlib.import_module(module_name)
if hasattr(module, 'main'):
module.main()
unreal.log(f"✅ 导入并执行 {module_name}")
reload_count += 1
except Exception as e:
unreal.log_error(f"导入 {module_name} 失败: {str(e)}")
error_count += 1
unreal.log(f"🔄 重载完成! 共处理 {reload_count} 个脚本, 其中 {error_count} 个失败")
# 初始化:扫描并运行所有脚本
scan_scripts()
import_and_run_scripts()
# 启动自动刷新
unreal.log("🔄 Python 自动刷新已启用")
# 在UE中,我们可以使用Tick来定时检查,这里我们使用一个简单的循环(在单独的线程中运行)
# 注意:在UE中,长时间运行的循环会阻塞主线程,所以我们需要使用异步或计时器
# 这里我们使用unreal.register_slate_post_tick_callback来注册一个每5秒执行一次的回调
tick_handle = None
last_check_time = 0
def check_scripts(timestamp):
global last_check_time
# 每5秒检查一次
if timestamp - last_check_time > 5.0:
last_check_time = timestamp
reload_scripts()
# 注册Slate后tick回调
tick_handle = unreal.register_slate_post_tick_callback(check_scripts)
# 注意:要停止自动刷新,可以注销回调:unreal.unregister_slate_post_tick_callback(tick_handle)
```
这个脚本做了以下改进:
1. 使用Pathlib处理路径,更清晰。
2. 在扫描脚本时,计算模块名(相对于基础目录)。
3. 在导入新脚本时,记录修改时间。
4. 在重载时,检查修改时间,只重载修改过的脚本。
5. 支持新增脚本的自动导入和运行。
注意:在UE中,使用`unreal.register_slate_post_tick_callback`来定期执行检查,这样不会阻塞主线程。
但是,请注意,这个脚本本身不应该被自动重载,因为它管理着重载逻辑。因此,我们应避免将它放在被监视的目录中,或者将其排除(例如,通过文件名,比如我们可以在扫描时跳过以`__`开头的文件,或者跳过特定的文件名)。
在这个例子中,我们跳过了以`__`开头的文件,所以如果这个脚本被命名为`__auto_reloader.py`,它就不会被自己重载。
如果这个脚本是手动执行的(比如在Python命令行中执行一次),那么它就会在后台运行,定期检查并重载其他脚本。
现在,我们回到错误本身:之前报错说重载失败,因为`main`不存在,并且处理了0个脚本。现在,我们改进了重载逻辑,应该可以避免这个问题。
如果问题仍然存在,请检查:
- 确保你的脚本中定义了`main`函数。
- 确保模块名计算正确(可以通过日志输出查看)。
我们可以在`scan_scripts`函数中打印出每个文件的模块名,以便调试。
希望这个修改能解决问题。
如果还有问题,请提供更多关于你的脚本结构的信息。
§§相关问题§§:
1. 如何避免自动重载脚本本身?
2. 在Unreal Engine中,如何安全地停止一个已经注册的tick回调?
3. 如何处理在重载过程中模块依赖关系?
4. 如果多个脚本之间存在依赖,自动重载会导致什么问题?如何解决?
</think>
### 解决Python脚本重载失败问题:Unreal Engine中的模块加载错误
#### 错误分析
错误信息 `Error: 重载失败 main: 'main'` 表明:
1. 脚本尝试调用名为 `main` 的函数但未找到
2. 可能原因:
- 脚本中缺少 `main()` 函数定义
- 函数命名不一致(如 `Main()` vs `main()`)
- 模块导入路径问题
- 脚本未正确初始化
#### 修复后的完整工具栏设置脚本
```python
import unreal
import os
import sys
import importlib
import time
from pathlib import Path
# ========================
# 配置区域 - 按需修改
# ========================
CONTENT_DIR = "C:/Users/EDY/Desktop/TA工具开发/Plugins/Python/Content/Python"
TOOLBAR_NAME = "TA工具箱"
RELOAD_INTERVAL = 5 # 自动重载间隔(秒)
# ========================
# 将脚本目录添加到系统路径
sys.path.insert(0, CONTENT_DIR)
# 存储已加载模块
loaded_modules = {}
last_mod_times = {}
def get_python_scripts():
"""获取所有Python脚本文件"""
scripts = []
for root, _, files in os.walk(CONTENT_DIR):
for file in files:
if file.endswith(".py") and not file.startswith("__"):
full_path = Path(root) / file
rel_path = str(full_path.relative_to(Path(CONTENT_DIR)))
module_name = rel_path.replace(".py", "").replace("\\", ".")
scripts.append((str(full_path), module_name))
return scripts
def load_or_reload_module(script_path, module_name):
"""加载或重载Python模块"""
try:
current_mtime = os.path.getmtime(script_path)
# 检查是否需要重载
if module_name in loaded_modules:
if last_mod_times.get(module_name, 0) >= current_mtime:
return loaded_modules[module_name]
unreal.log(f"重载模块: {module_name}")
module = importlib.reload(loaded_modules[module_name])
else:
unreal.log(f"加载模块: {module_name}")
module = importlib.import_module(module_name)
# 更新状态
loaded_modules[module_name] = module
last_mod_times[module_name] = current_mtime
# 初始化模块
if hasattr(module, "initialize"):
module.initialize()
unreal.log(f"初始化: {module_name}")
return module
except Exception as e:
unreal.log_error(f"加载 {module_name} 失败: {str(e)}")
return None
def create_toolbar():
"""创建主工具栏"""
menus = unreal.ToolMenus.get()
main_menu = menus.find_menu("LevelEditor.LevelEditorToolBar")
if not main_menu:
unreal.log_error("找不到主工具栏!")
return
# 清除现有入口
main_menu.remove_section("PythonTools")
# 添加新工具栏段
section = main_menu.add_section("PythonTools", f"{TOOLBAR_NAME}")
# 添加工具栏按钮
entry = unreal.ToolMenuEntry(
name="PythonTools_Main",
type=unreal.MultiBlockType.TOOL_BAR_BUTTON,
insert_position=unreal.ToolMenuInsert("", unreal.ToolMenuInsertType.DEFAULT)
)
entry.set_label("TA工具箱")
entry.set_icon("EditorStyle", "LevelEditor.OpenCinematic")
entry.set_string_command(
unreal.ToolMenuStringCommandType.PYTHON,
"",
"from toolbar_setup import open_tools_menu; open_tools_menu()"
)
section.add_entry(entry)
def open_tools_menu():
"""打开工具菜单"""
print("工具栏按钮点击!")
# 创建菜单
menu = unreal.ToolMenus.get().generate_widget(
"LevelEditor.LevelEditorToolBar.PythonTools"
)
# 显示菜单
unreal.PyToolkitDialog.spawn_dialog(
"TA工具箱",
menu,
unreal.PyToolkitDialogSize(400, 300)
)
def reload_all_scripts():
"""重载所有脚本"""
unreal.log("=" * 50)
unreal.log("🔄 开始重载脚本...")
start_time = time.time()
scripts = get_python_scripts()
success_count = 0
fail_count = 0
for script_path, module_name in scripts:
module = load_or_reload_module(script_path, module_name)
if module:
success_count += 1
else:
fail_count += 1
unreal.log(f"✅ 重载完成! 成功: {success_count}, 失败: {fail_count}")
unreal.log(f"⏱️ 耗时: {time.time() - start_time:.2f}秒")
unreal.log("=" * 50)
# ========================
# 主程序入口
# ========================
if __name__ == "__main__":
# 创建工具栏
create_toolbar()
unreal.log(f"已创建工具栏: {TOOLBAR_NAME}")
# 初始加载所有脚本
reload_all_scripts()
# 启动自动重载定时器
unreal.log(f"🔁 启用自动重载 (每 {RELOAD_INTERVAL} 秒)")
unreal.register_slate_post_tick_callback(
lambda delta_time: reload_all_scripts() if int(time.time()) % RELOAD_INTERVAL == 0 else None
)
```
#### 关键修复点解释:
1. **`main`函数缺失问题**
- 使用标准入口检查 `if __name__ == "__main__":` 替代 `main()` 函数
- 添加模块初始化函数 `initialize()` 替代 `main`
2. **模块加载优化**
- 使用 `importlib` 动态加载和重载模块
- 记录文件修改时间避免不必要重载
- 添加错误处理防止单个脚本失败影响整体
3. **工具栏创建改进**
- 使用 `unreal.ToolMenuEntry` 创建工具栏按钮
- 添加图标和标签增强可视化
- 实现点击事件 `open_tools_menu()`
4. **自动重载系统**
- 使用 `register_slate_post_tick_callback` 实现定时检查
- 添加详细日志记录重载状态
- 支持按时间间隔自动重载
#### 验证步骤:
1. 将脚本保存为 `toolbar_setup.py`
2. 在Unreal Engine中执行:
```python
import toolbar_setup
toolbar_setup.reload_all_scripts()
```
3. 检查输出日志是否显示成功加载