主题
SCE 编辑器 C# 插件开发指南
插件系统采用属性驱动、零配置设计,一个类加几个 Attribute 即可构成完整插件。
核心特性:
[EditorPlugin]+[MCPTool]+[MenuItem]声明式定义[MCPTool]方法自动暴露为 MCP 工具,AI 可直接调用[MenuItem]方法自动注册到编辑器标题菜单栏- 通过
IPluginContext访问受控的服务子集
前提条件:.NET 9.0 SDK + PluginSDK.dll
快速开始
插件是引用 PluginSDK.dll 的 C# 类库。编译后将 DLL 放到编辑器 Plugins/ 目录,启动编辑器自动加载。
最小插件模板
csharp
using SCE.PluginSDK;
[EditorPlugin("MyPlugin", "插件功能描述")]
public class MyPlugin : IEditorPlugin
{
private IPluginContext _ctx = null!;
public void Initialize(IPluginContext context) => _ctx = context;
public void Shutdown() { }
[MCPTool("my_tool_name", "工具功能描述")]
public string MyTool(
[Param("input", "输入参数说明")] string input)
{
return $"Result: {input}";
}
}项目文件模板
xml
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Platforms>x64</Platforms>
</PropertyGroup>
<ItemGroup>
<Reference Include="PluginSDK">
<HintPath>path\to\PluginSDK.dll</HintPath>
</Reference>
</ItemGroup>
</Project>Attribute 参考
[EditorPlugin(name, description)] -- 插件声明(必需,标记在类上)
必须同时实现 IEditorPlugin 接口。
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
Name | string | 必填 | 插件显示名称 |
Description | string | 必填 | 插件功能描述 |
Version | string | "1.0" | 版本号 |
Author | string | "" | 作者名 |
[MCPTool(name, description)] -- MCP 工具注册(标记在方法上)
工具名自动添加 plugin_ 前缀(如 "my_tool" -> "plugin_my_tool")。
| 返回类型 | 行为 |
|---|---|
string | 直接作为文本返回 |
object / 任意类 | 序列化为 JSON |
Task<string> / Task<object> | 异步版本 |
void / Task | 返回 "OK" |
[Param(description)] 或 [Param(name, description)] -- 参数描述
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
Name | string? | C# 参数名 | 覆盖参数名称 |
Description | string | 必填 | 参数说明,对 AI 可见 |
Required | bool | true | 设为 false 表示可选 |
支持类型:string, int, long, float, double, bool, 数组, 对象。
[MenuItem(path)] -- 菜单项注册(标记在无参方法上)
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
Path | string | 必填 | 菜单路径,如 "插件/数据工具/批量重命名" |
Shortcut | string? | null | 快捷键,如 "Ctrl+Shift+R" |
[EditorCommand(commandId)] -- 编辑器命令(规划中)
IPluginContext 服务接口
csharp
public interface IPluginContext
{
IDataAccess Data; // 游戏数据表操作
ITriggerAccess Triggers; // 触发器系统
IPluginLogger Log; // 日志输出
IEditorAccess Editor; // 编辑器状态
}IDataAccess
csharp
IReadOnlyList<string> GetTableNames();
IReadOnlyList<int> GetEntryIds(string tableName);
object? GetFieldValue(string tableName, int entryId, string fieldName);
bool SetFieldValue(string tableName, int entryId, string fieldName, object? value);
bool TableExists(string tableName);ITriggerAccess
csharp
IReadOnlyList<string> GetFileNames(string? env = null);
IReadOnlyList<string> Search(string query, string? nodeType = null, string? env = null);
bool IsAvailable { get; }IEditorAccess
csharp
bool IsMapLoaded { get; }
string? ProjectPath { get; }IPluginLogger
csharp
void Info(string message);
void Warn(string message);
void Error(string message);
void Debug(string message);运行机制
- 编辑器加载地图时初始化插件系统
- 扫描
Plugins/目录中所有 DLL,查找[EditorPlugin]类 - 调用每个插件的
Initialize(context) - 反射
[MCPTool]方法,生成 JSON Schema 注册到 MCP 工具列表 - 反射
[MenuItem]方法,注册到编辑器菜单栏 - AI 可通过 MCP 调用
plugin_xxx工具
最佳实践
- 一个 DLL 一个插件
- 工具命名用 snake_case(自动加
plugin_前缀) - 在返回值中包含错误信息,不要抛异常
- 用
_ctx.Log输出日志,禁止Console.WriteLine - I/O 密集型操作用
Task<string>返回类型 - 测试:DLL 放入
Plugins/,以--mcp-stdio或--mcp-port=8765启动,用tools/list验证
完整示例
csharp
using SCE.PluginSDK;
[EditorPlugin("DataBatch", "游戏数据表批量操作工具")]
public class DataBatchPlugin : IEditorPlugin
{
private IPluginContext _ctx = null!;
public void Initialize(IPluginContext context) => _ctx = context;
public void Shutdown() { }
[MCPTool("batch_set_field", "批量设置数据表中所有条目的某个字段值")]
public object BatchSetField(
[Param("table", "数据表名")] string table,
[Param("field", "要设置的字段名")] string field,
[Param("value", "要设置的值")] string value)
{
if (!_ctx.Data.TableExists(table))
return new { error = $"数据表 '{table}' 不存在" };
var ids = _ctx.Data.GetEntryIds(table);
int updated = 0;
foreach (var id in ids)
{
if (_ctx.Data.SetFieldValue(table, id, field, value))
updated++;
}
return new { success = true, updated, total = ids.Count };
}
[MCPTool("batch_find_entries", "查找字段值匹配的条目")]
public object FindEntries(
[Param("table", "数据表名")] string table,
[Param("field", "要检查的字段名")] string field,
[Param("value", "要匹配的值")] string value)
{
if (!_ctx.Data.TableExists(table))
return new { error = $"数据表 '{table}' 不存在" };
var ids = _ctx.Data.GetEntryIds(table);
var matches = new List<int>();
foreach (var id in ids)
{
var fieldValue = _ctx.Data.GetFieldValue(table, id, field);
if (fieldValue?.ToString() == value)
matches.Add(id);
}
return new { matches, count = matches.Count };
}
[MenuItem("插件/数据批量工具/显示统计")]
public void ShowStats()
{
var tables = _ctx.Data.GetTableNames();
_ctx.Log.Info($"当前共 {tables.Count} 张数据表");
}
}