feat: Add WorldStateFlagAttribute and custom property drawer for enhanced dialogue management

- Implemented WorldStateFlagAttribute to mark string fields as world state flags.
- Created NarrativeNPCEditor for custom inspector to visualize dialogue version activation states.
- Developed WorldStateFlagDrawer to provide dropdown menu for known flags in the inspector.
- Introduced ActorModule for managing DialogueActorSO assets, including viewing, creating, and deleting actors.
- Added DialogueModule for managing DialogueSequenceSO assets with detailed previews and action bars.
- Established QuestModule for managing QuestSO assets, including objectives and branches.
- Implemented QuestManagerPostprocessor to automatically refresh QuestManager's quest list on asset changes.
This commit is contained in:
2026-05-24 00:36:11 +08:00
parent 520f84999b
commit 446fd5dcd0
22 changed files with 1908 additions and 101 deletions

View File

@@ -0,0 +1,128 @@
using System;
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
using BaseGames.Dialogue;
namespace BaseGames.Editor.Modules
{
/// <summary>
/// DataHub 对话角色模块 —— 管理 DialogueActorSO 资产。
/// 统一查看、创建、重命名、删除 NPC/玩家角色定义(头像、名称 Key、强调色
/// </summary>
public class ActorModule : IDataModule
{
private const string Folder = "Assets/_Game/Data/Dialogue/Actors";
private const string Prefix = "Actor_";
public string ModuleId => "actor";
public string DisplayName => "角色";
public string IconName => "d_Prefab Icon";
private SoListPane<DialogueActorSO> _listPane;
private DetailHeader _header;
private DialogueActorSO _selected;
public void Initialize()
{
_listPane = new SoListPane<DialogueActorSO>(
Folder, Prefix,
a => a.isPlayer ? "[玩家]" : null);
}
public void BuildListPane(VisualElement container, Action<UnityEngine.Object> onSelected)
{
_listPane.SelectionChanged = sel =>
{
_selected = sel;
onSelected?.Invoke(sel);
};
container.Add(_listPane);
_listPane.Refresh();
}
public void BuildDetailPane(VisualElement container, UnityEngine.Object selected)
{
_selected = selected as DialogueActorSO;
_header = new DetailHeader();
_header.SetAsset(_selected);
_header.RenameRequested += OnRenameRequested;
container.Add(_header);
if (_selected == null) return;
container.Add(BuildInfoCard(_selected));
container.Add(BuildActionBar(_selected));
container.Add(SkillModule.MakeDivider());
container.Add(new InspectorElement(_selected));
}
public void OnActivated() => _listPane?.Refresh();
// ── 内部 ─────────────────────────────────────────────────────────────
private void OnRenameRequested(string newName)
{
if (_selected == null) return;
var (ok, err) = AssetOperations.Rename(_selected, newName);
if (!ok) EditorUtility.DisplayDialog("重命名失败", err, "确定");
else { _header.SetAsset(_selected); _listPane.Invalidate(); }
}
private static VisualElement BuildInfoCard(DialogueActorSO a)
{
var card = SkillModule.MakeCard();
SkillModule.AddChip(card, "ID", string.IsNullOrEmpty(a.actorId) ? "(未设置)" : a.actorId);
SkillModule.AddChip(card, "名称 Key", string.IsNullOrEmpty(a.nameKey) ? "(未设置)" : a.nameKey);
if (a.isPlayer)
SkillModule.AddChip(card, "类型", "玩家");
// 头像预览
if (a.portrait != null)
{
var row = new VisualElement();
row.style.flexDirection = FlexDirection.Row;
row.style.alignItems = Align.Center;
row.style.paddingLeft = 8;
row.style.paddingTop = 4;
var img = new Image { image = a.portrait.texture };
img.style.width = 40;
img.style.height = 40;
img.style.borderTopLeftRadius = 4;
img.style.borderTopRightRadius = 4;
img.style.borderBottomLeftRadius = 4;
img.style.borderBottomRightRadius = 4;
row.Add(img);
// 强调色色块
var swatch = new VisualElement();
swatch.style.width = 14;
swatch.style.height = 14;
swatch.style.marginLeft = 8;
swatch.style.backgroundColor = new StyleColor(a.accentColor);
swatch.style.borderTopLeftRadius = 3;
swatch.style.borderTopRightRadius = 3;
swatch.style.borderBottomLeftRadius = 3;
swatch.style.borderBottomRightRadius = 3;
row.Add(swatch);
card.Add(row);
}
return card;
}
private VisualElement BuildActionBar(DialogueActorSO a)
{
return SkillModule.BuildStandardActionBar(
a, Folder, Prefix,
onCreated: c => _listPane.Refresh(c),
onCloned: c => _listPane.Refresh(c),
onDeleted: () => _listPane.Refresh(null));
}
}
}

View File

@@ -0,0 +1,230 @@
using System;
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
using BaseGames.Dialogue;
namespace BaseGames.Editor.Modules
{
/// <summary>
/// DataHub 对话序列模块 —— 管理 DialogueSequenceSO 资产。
/// </summary>
public class DialogueModule : IDataModule
{
private const string Folder = "Assets/_Game/Data/Dialogue";
private const string Prefix = "DLG_";
public string ModuleId => "dialogue";
public string DisplayName => "对话";
public string IconName => "d_UnityEditor.ConsoleWindow";
private SoListPane<DialogueSequenceSO> _listPane;
private DetailHeader _header;
private DialogueSequenceSO _selected;
public void Initialize()
{
_listPane = new SoListPane<DialogueSequenceSO>(
Folder, Prefix,
s =>
{
int v = s.variants != null ? s.variants.Length : 0;
return v > 0 ? $"{v}变体" : null;
});
}
public void BuildListPane(VisualElement container, Action<UnityEngine.Object> onSelected)
{
_listPane.SelectionChanged = sel =>
{
_selected = sel;
onSelected?.Invoke(sel);
};
container.Add(_listPane);
_listPane.Refresh();
}
public void BuildDetailPane(VisualElement container, UnityEngine.Object selected)
{
_selected = selected as DialogueSequenceSO;
_header = new DetailHeader();
_header.SetAsset(_selected);
_header.RenameRequested += OnRenameRequested;
container.Add(_header);
if (_selected == null) return;
container.Add(BuildInfoCard(_selected));
container.Add(BuildLinesPreview(_selected));
if (_selected.variants != null && _selected.variants.Length > 0)
container.Add(BuildVariantsCard(_selected));
container.Add(BuildActionBar(_selected));
container.Add(SkillModule.MakeDivider());
container.Add(new InspectorElement(_selected));
}
public void OnActivated() => _listPane?.Refresh();
// ── 内部 ─────────────────────────────────────────────────────────────
private void OnRenameRequested(string newName)
{
if (_selected == null) return;
var (ok, err) = AssetOperations.Rename(_selected, newName);
if (!ok) EditorUtility.DisplayDialog("重命名失败", err, "确定");
else { _header.SetAsset(_selected); _listPane.Invalidate(); }
}
private static VisualElement BuildInfoCard(DialogueSequenceSO s)
{
var card = SkillModule.MakeCard();
int lineCount = s.lines != null ? s.lines.Length : 0;
int variantCount = s.variants != null ? s.variants.Length : 0;
SkillModule.AddChip(card, "ID", string.IsNullOrEmpty(s.sequenceId) ? "(未设置)" : s.sequenceId);
SkillModule.AddChip(card, "行数", lineCount.ToString());
if (variantCount > 0)
SkillModule.AddChip(card, "变体数", variantCount.ToString());
return card;
}
private static VisualElement BuildLinesPreview(DialogueSequenceSO s)
{
var section = new VisualElement();
section.style.paddingLeft = 12;
section.style.paddingRight = 12;
section.style.paddingTop = 6;
section.style.paddingBottom = 6;
var title = new Label("对话预览(前 5 行)");
title.style.fontSize = 11;
title.style.opacity = 0.55f;
title.style.marginBottom = 4;
title.style.unityFontStyleAndWeight = FontStyle.Bold;
section.Add(title);
if (s.lines == null || s.lines.Length == 0)
{
var empty = new Label("(无对话行)");
empty.style.opacity = 0.4f;
empty.style.fontSize = 11;
section.Add(empty);
return section;
}
int preview = Mathf.Min(5, s.lines.Length);
for (int i = 0; i < preview; i++)
{
var line = s.lines[i];
var row = new VisualElement();
row.style.flexDirection = FlexDirection.Row;
row.style.alignItems = Align.Center;
row.style.marginBottom = 3;
// 头像图标actor 优先,回退到直接字段)
var portrait = line.ResolvedPortrait;
if (portrait != null)
{
var img = new Image { image = portrait.texture };
img.style.width = 18;
img.style.height = 18;
img.style.marginRight = 4;
img.style.borderTopLeftRadius = 2;
img.style.borderTopRightRadius = 2;
img.style.borderBottomLeftRadius = 2;
img.style.borderBottomRightRadius = 2;
row.Add(img);
}
// 语音图标
if (line.voiceClip != null)
{
var ico = EditorGUIUtility.IconContent("d_AudioClip Icon");
if (ico?.image != null)
{
var img = new Image { image = ico.image };
img.style.width = 14;
img.style.height = 14;
img.style.marginRight = 4;
row.Add(img);
}
}
// 说话人actor 优先,回退到直接字段)
string speakerKey = line.ResolvedNameKey;
if (!string.IsNullOrEmpty(speakerKey))
{
var spk = new Label(speakerKey + ":");
spk.style.fontSize = 11;
spk.style.opacity = 0.55f;
spk.style.unityFontStyleAndWeight = FontStyle.Bold;
spk.style.marginRight = 4;
spk.style.flexShrink = 0;
row.Add(spk);
}
// 文本 key尝试显示本地化实际内容回退到 key 本身)
string rawText = string.IsNullOrEmpty(line.textKey) ? "(空)" : line.textKey;
string preview = string.IsNullOrEmpty(line.textKey)
? "(空)"
: (BaseGames.Localization.LocalizationManager.GetEditorPreview(line.textKey, "Dialogue") ?? rawText);
if (preview.Length > 48) preview = preview[..48] + "…";
var lbl = new Label(preview);
lbl.style.fontSize = 11;
lbl.style.overflow = Overflow.Hidden;
row.Add(lbl);
section.Add(row);
}
if (s.lines.Length > preview)
{
var more = new Label($"… 还有 {s.lines.Length - preview} 行");
more.style.opacity = 0.4f;
more.style.fontSize = 10;
section.Add(more);
}
return section;
}
private static VisualElement BuildVariantsCard(DialogueSequenceSO s)
{
var card = SkillModule.MakeCard();
card.style.flexDirection = FlexDirection.Column;
var title = new Label("条件变体");
title.style.fontSize = 11;
title.style.opacity = 0.55f;
title.style.marginBottom = 4;
title.style.unityFontStyleAndWeight = FontStyle.Bold;
card.Add(title);
foreach (var v in s.variants)
{
var row = new VisualElement();
row.style.flexDirection = FlexDirection.Row;
row.style.marginBottom = 2;
string flags = v.requiredFlags != null && v.requiredFlags.Length > 0
? string.Join(", ", v.requiredFlags)
: "(无条件)";
SkillModule.AddChip(row, "条件", flags);
SkillModule.AddChip(row, "替换序列", v.sequence != null ? v.sequence.name : "(未设置)");
card.Add(row);
}
return card;
}
private VisualElement BuildActionBar(DialogueSequenceSO s)
{
return SkillModule.BuildStandardActionBar(
s, Folder, Prefix,
onCreated: c => _listPane.Refresh(c),
onCloned: c => _listPane.Refresh(c),
onDeleted: () => _listPane.Refresh(null));
}
}
}

View File

@@ -0,0 +1,342 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
using BaseGames.Quest;
namespace BaseGames.Editor.Modules
{
/// <summary>
/// DataHub 任务模块 —— 管理 QuestSO 资产。
/// </summary>
public class QuestModule : IDataModule
{
private const string Folder = "Assets/_Game/Data/Quest";
private const string Prefix = "Quest_";
public string ModuleId => "quest";
public string DisplayName => "任务";
public string IconName => "d_UnityEditor.InspectorWindow";
private SoListPane<QuestSO> _listPane;
private DetailHeader _header;
private QuestSO _selected;
public void Initialize()
{
_listPane = new SoListPane<QuestSO>(
Folder, Prefix,
s =>
{
bool hasPre = s.prerequisiteQuests != null && s.prerequisiteQuests.Length > 0;
return hasPre ? "有前置" : null;
});
}
public void BuildListPane(VisualElement container, Action<UnityEngine.Object> onSelected)
{
_listPane.SelectionChanged = sel =>
{
_selected = sel;
onSelected?.Invoke(sel);
};
container.Add(_listPane);
_listPane.Refresh();
}
public void BuildDetailPane(VisualElement container, UnityEngine.Object selected)
{
_selected = selected as QuestSO;
_header = new DetailHeader();
_header.SetAsset(_selected);
_header.RenameRequested += OnRenameRequested;
container.Add(_header);
if (_selected == null) return;
container.Add(BuildInfoCard(_selected));
container.Add(BuildObjectivesList(_selected));
if (_selected.branches != null && _selected.branches.Length > 0)
container.Add(BuildBranchesCard(_selected));
container.Add(BuildActionBar(_selected));
container.Add(SkillModule.MakeDivider());
container.Add(new InspectorElement(_selected));
}
public void OnActivated() => _listPane?.Refresh();
// ── 内部 ─────────────────────────────────────────────────────────────
private void OnRenameRequested(string newName)
{
if (_selected == null) return;
var (ok, err) = AssetOperations.Rename(_selected, newName);
if (!ok) EditorUtility.DisplayDialog("重命名失败", err, "确定");
else { _header.SetAsset(_selected); _listPane.Invalidate(); }
}
private static VisualElement BuildInfoCard(QuestSO s)
{
var card = SkillModule.MakeCard();
int objCount = s.objectives != null ? s.objectives.Length : 0;
SkillModule.AddChip(card, "ID", string.IsNullOrEmpty(s.questId) ? "(未设置)" : s.questId);
SkillModule.AddChip(card, "名称 Key", string.IsNullOrEmpty(s.displayNameKey) ? "(未设置)" : s.displayNameKey);
if (!string.IsNullOrEmpty(s.descriptionKey))
SkillModule.AddChip(card, "描述 Key", s.descriptionKey);
SkillModule.AddChip(card, "目标数", objCount.ToString());
if (s.prerequisiteQuests != null && s.prerequisiteQuests.Length > 0)
{
// 显示每个前置任务的 questId方便策划一眼看清依赖链
var preIds = new System.Text.StringBuilder();
foreach (var pre in s.prerequisiteQuests)
{
if (pre == null) continue;
if (preIds.Length > 0) preIds.Append(", ");
preIds.Append(string.IsNullOrEmpty(pre.questId) ? pre.name : pre.questId);
}
if (preIds.Length > 0)
SkillModule.AddChip(card, "前置任务", preIds.ToString());
}
if (s.minAffinityToAccept > 0)
SkillModule.AddChip(card, "好感门槛", s.minAffinityToAccept.ToString());
if (s.canFail)
SkillModule.AddChip(card, "可失败", "✓");
if (s.reward != null)
SkillModule.AddChip(card, "奖励", s.reward.name);
return card;
}
private static VisualElement BuildObjectivesList(QuestSO s)
{
var section = new VisualElement();
section.style.paddingLeft = 12;
section.style.paddingRight = 12;
section.style.paddingTop = 6;
section.style.paddingBottom = 6;
var title = new Label("目标列表");
title.style.fontSize = 11;
title.style.opacity = 0.55f;
title.style.marginBottom = 4;
title.style.unityFontStyleAndWeight = FontStyle.Bold;
section.Add(title);
if (s.objectives == null || s.objectives.Length == 0)
{
var empty = new Label("(无目标)");
empty.style.opacity = 0.4f;
empty.style.fontSize = 11;
section.Add(empty);
return section;
}
for (int i = 0; i < s.objectives.Length; i++)
{
var obj = s.objectives[i];
if (obj == null) continue;
var row = new VisualElement();
row.style.flexDirection = FlexDirection.Row;
row.style.alignItems = Align.Center;
row.style.marginBottom = 3;
// 序号
var idx = new Label($"{i + 1}.");
idx.style.fontSize = 11;
idx.style.opacity = 0.5f;
idx.style.marginRight = 4;
idx.style.width = 16;
idx.style.flexShrink = 0;
row.Add(idx);
// 类型徽章
string badge = obj.BadgeLabel;
var badgeLbl = new Label(badge);
badgeLbl.style.fontSize = 10;
badgeLbl.style.opacity = 0.7f;
badgeLbl.style.marginRight = 6;
badgeLbl.style.flexShrink = 0;
badgeLbl.style.unityFontStyleAndWeight = FontStyle.Bold;
row.Add(badgeLbl);
// ID
string id = string.IsNullOrEmpty(obj.objectiveId) ? obj.name : obj.objectiveId;
var idLbl = new Label(id);
idLbl.style.fontSize = 11;
idLbl.style.flexGrow = 1;
row.Add(idLbl);
// 可选标记
if (obj.IsOptional)
{
var opt = new Label("[可选]");
opt.style.fontSize = 10;
opt.style.opacity = 0.5f;
opt.style.marginLeft = 4;
row.Add(opt);
}
section.Add(row);
}
return section;
}
private static VisualElement BuildBranchesCard(QuestSO s)
{
var card = SkillModule.MakeCard();
card.style.flexDirection = FlexDirection.Column;
var title = new Label("完成后分支");
title.style.fontSize = 11;
title.style.opacity = 0.55f;
title.style.marginBottom = 4;
title.style.unityFontStyleAndWeight = FontStyle.Bold;
card.Add(title);
foreach (var branch in s.branches)
{
var row = new VisualElement();
row.style.flexDirection = FlexDirection.Row;
row.style.marginBottom = 2;
string condition = branch.conditionQuest != null ? branch.conditionQuest.questId : "(默认)";
string next = branch.nextQuest != null ? branch.nextQuest.name : "(无)";
SkillModule.AddChip(row, "条件", condition);
SkillModule.AddChip(row, "后续任务", next);
// 优先显示新 SO 引用回退到旧字段Obsolete
string seqName = branch.npcDialogueSequence != null
? branch.npcDialogueSequence.name
#pragma warning disable CS0618
: branch.npcDialogueKey;
#pragma warning restore CS0618
if (!string.IsNullOrEmpty(seqName))
SkillModule.AddChip(row, "对话序列", seqName);
card.Add(row);
}
return card;
}
private VisualElement BuildActionBar(QuestSO s)
{
var bar = SkillModule.BuildStandardActionBar(
s, Folder, Prefix,
onCreated: c => _listPane.Refresh(c),
onCloned: c => _listPane.Refresh(c),
onDeleted: () => _listPane.Refresh(null));
// 任务模块额外:代码常量生成
new Button(GenerateQuestKeys) { text = "生成常量" }.AlsoAddTo(bar);
return bar;
}
// ── QuestKeys.cs 常量生成器 ──────────────────────────────────────────
private const string GeneratedFolder = "Assets/_Game/Scripts/Generated";
private const string QuestKeysPath = GeneratedFolder + "/QuestKeys.cs";
private static void GenerateQuestKeys()
{
// 收集所有 questId
var questIds = new SortedSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var q in AssetOperations.FindAll<QuestSO>())
if (!string.IsNullOrWhiteSpace(q.questId))
questIds.Add(q.questId.Trim());
// 收集所有 targetNpcId来自 TalkToNPC 目标 SO
var npcIds = new SortedSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var obj in AssetOperations.FindAll<TalkToNPCObjective>())
if (!string.IsNullOrWhiteSpace(obj.targetNpcId))
npcIds.Add(obj.targetNpcId.Trim());
var sb = new StringBuilder();
sb.AppendLine("// <auto-generated>");
sb.AppendLine("// 由 QuestModule 工具自动生成,请勿手动编辑。");
sb.AppendLine("// 重新生成DataHub → 任务 → 任意任务 → 生成常量");
sb.AppendLine("// </auto-generated>");
sb.AppendLine("namespace BaseGames.Quest");
sb.AppendLine("{");
sb.AppendLine(" /// <summary>任务 ID 常量(从 QuestSO 自动生成)。</summary>");
sb.AppendLine(" public static class QuestKeys");
sb.AppendLine(" {");
sb.AppendLine(" /// <summary>任务唯一 ID 常量。</summary>");
sb.AppendLine(" public static class Quest");
sb.AppendLine(" {");
if (questIds.Count == 0)
sb.AppendLine(" // (未发现任何 QuestSO");
foreach (var id in questIds)
sb.AppendLine($" public const string {ToIdentifier(id)} = \"{id}\";");
sb.AppendLine(" }");
sb.AppendLine();
sb.AppendLine(" /// <summary>TalkToNPC 目标中使用的 NPC ID 常量。</summary>");
sb.AppendLine(" public static class NpcId");
sb.AppendLine(" {");
if (npcIds.Count == 0)
sb.AppendLine(" // (未发现任何 TalkToNPCObjective");
foreach (var id in npcIds)
sb.AppendLine($" public const string {ToIdentifier(id)} = \"{id}\";");
sb.AppendLine(" }");
sb.AppendLine(" }");
sb.AppendLine("}");
if (!Directory.Exists(GeneratedFolder))
Directory.CreateDirectory(GeneratedFolder);
File.WriteAllText(QuestKeysPath, sb.ToString(), Encoding.UTF8);
AssetDatabase.Refresh();
Debug.Log($"[QuestModule] QuestKeys.cs 已生成:{questIds.Count} 个任务 ID{npcIds.Count} 个 NPC ID。");
EditorUtility.DisplayDialog("生成成功",
$"QuestKeys.cs 已写入 {QuestKeysPath}\n任务 ID: {questIds.Count} NPC ID: {npcIds.Count}",
"确定");
}
/// <summary>将任意字符串转换为合法的 C# 标识符PascalCase。C# 保留关键字加 @ 前缀。</summary>
private static string ToIdentifier(string raw)
{
if (string.IsNullOrEmpty(raw)) return "_Empty";
var parts = raw.Split('_', '-', ' ', '.', '/');
var sb = new StringBuilder();
foreach (var part in parts)
{
if (part.Length == 0) continue;
sb.Append(char.ToUpperInvariant(part[0]));
if (part.Length > 1) sb.Append(part.Substring(1));
}
string result = sb.ToString();
if (result.Length > 0 && char.IsDigit(result[0]))
result = "_" + result;
if (string.IsNullOrEmpty(result)) return "_Empty";
// C# 保留关键字加 @ 前缀,避免生成无法编译的代码
if (s_CSharpKeywords.Contains(result))
result = "@" + result;
return result;
}
private static readonly HashSet<string> s_CSharpKeywords = new HashSet<string>(
System.StringComparer.Ordinal)
{
"abstract","as","base","bool","break","byte","case","catch","char","checked",
"class","const","continue","decimal","default","delegate","do","double","else",
"enum","event","explicit","extern","false","finally","fixed","float","for",
"foreach","goto","if","implicit","in","int","interface","internal","is","lock",
"long","namespace","new","null","object","operator","out","override","params",
"private","protected","public","readonly","ref","return","sbyte","sealed",
"short","sizeof","stackalloc","static","string","struct","switch","this",
"throw","true","try","typeof","uint","ulong","unchecked","unsafe","ushort",
"using","virtual","void","volatile","while"
};
}
}

View File

@@ -99,15 +99,7 @@ namespace BaseGames.Editor.Modules
{ text = "克隆..." }.AlsoAddTo(bar);
var del = new Button(() => { if (AssetOperations.Delete(s)) _listPane.Refresh(null); }) { text = "删除" };
del.style.borderLeftColor = new StyleColor(new Color(0.8f, 0.3f, 0.3f, 0.6f));
del.style.borderRightColor = new StyleColor(new Color(0.8f, 0.3f, 0.3f, 0.6f));
del.style.borderTopColor = new StyleColor(new Color(0.8f, 0.3f, 0.3f, 0.6f));
del.style.borderBottomColor = new StyleColor(new Color(0.8f, 0.3f, 0.3f, 0.6f));
del.style.borderLeftWidth = 1;
del.style.borderRightWidth = 1;
del.style.borderTopWidth = 1;
del.style.borderBottomWidth = 1;
del.style.marginLeft = 8;
ApplyDeleteStyle(del);
del.AlsoAddTo(bar);
return bar;
@@ -171,6 +163,69 @@ namespace BaseGames.Editor.Modules
d.style.backgroundColor = new StyleColor(new Color(0.5f, 0.5f, 0.5f, 0.2f));
return d;
}
/// <summary>将删除按钮染成红色边框,统一各模块样式。</summary>
internal static void ApplyDeleteStyle(Button btn)
{
var red = new StyleColor(new Color(0.8f, 0.3f, 0.3f, 0.6f));
btn.style.borderLeftColor = red;
btn.style.borderRightColor = red;
btn.style.borderTopColor = red;
btn.style.borderBottomColor = red;
btn.style.borderLeftWidth = 1;
btn.style.borderRightWidth = 1;
btn.style.borderTopWidth = 1;
btn.style.borderBottomWidth = 1;
btn.style.marginLeft = 8;
}
/// <summary>
/// 为任意 ScriptableObject 模块生成标准 ActionBar新建 / 定位 / 克隆 / 删除)。
/// 各模块可在返回后向 bar 追加额外按钮。
/// </summary>
/// <param name="asset">当前选中资产。</param>
/// <param name="folder">资产所在文件夹(用于新建 / 克隆)。</param>
/// <param name="prefix">新建资产的文件名前缀。</param>
/// <param name="onCreated">新建完成回调(传入新资产)。</param>
/// <param name="onCloned">克隆完成回调(传入克隆资产)。</param>
/// <param name="onDeleted">删除完成回调。</param>
internal static VisualElement BuildStandardActionBar<T>(
T asset,
string folder,
string prefix,
Action<T> onCreated,
Action<T> onCloned,
Action onDeleted) where T : UnityEngine.ScriptableObject
{
var bar = MakeActionBar();
new Button(() =>
{
var c = AssetOperations.Create<T>(folder, prefix + "New");
if (c != null) onCreated?.Invoke(c);
}) { text = "新建" }.AlsoAddTo(bar);
new Button(() =>
{
EditorGUIUtility.PingObject(asset);
Selection.activeObject = asset;
}) { text = "定位" }.AlsoAddTo(bar);
new Button(() =>
{
var c = AssetOperations.Clone(asset, folder);
if (c != null) onCloned?.Invoke(c);
}) { text = "克隆..." }.AlsoAddTo(bar);
var del = new Button(() =>
{
if (AssetOperations.Delete(asset)) onDeleted?.Invoke();
}) { text = "删除" };
ApplyDeleteStyle(del);
del.AlsoAddTo(bar);
return bar;
}
}
// ── Button 扩展(模块内共用)─────────────────────────────────────────────