摄像机区域的架构改动

This commit is contained in:
2026-05-15 14:47:24 +08:00
parent 1b37297585
commit f264329751
3591 changed files with 1687228 additions and 446503 deletions

View File

@@ -0,0 +1,276 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
using BaseGames.Combat;
using BaseGames.Skills;
namespace BaseGames.Editor.Skills
{
/// <summary>
/// 技能数据管理窗口W-03
/// 技术UI Toolkit TwoPaneSplitView。
/// 菜单BaseGames / Data / Skill Editor
///
/// 左栏:可搜索的 FormSkillSO 列表,按 SkillEffectType 分组过滤。
/// 右栏:选中技能的完整属性编辑 + HitBox Prefab 结构校验 + 底部资源消耗预览。
/// </summary>
public class SkillEditorWindow : EditorWindow
{
private static readonly StyleSheet _sharedUSS;
private static readonly string[] _effectTypeOptions;
static SkillEditorWindow()
{
_sharedUSS = AssetDatabase.LoadAssetAtPath<StyleSheet>(
"Assets/_Game/Scripts/Editor/UIToolkit/Editor.uss");
var names = Enum.GetNames(typeof(SkillEffectType));
_effectTypeOptions = new string[names.Length + 1];
_effectTypeOptions[0] = "全部";
Array.Copy(names, 0, _effectTypeOptions, 1, names.Length);
}
[MenuItem("BaseGames/Data/Skill Editor", priority = 101)]
public static void Open()
{
var wnd = GetWindow<SkillEditorWindow>();
wnd.titleContent = new GUIContent("Skill Editor");
wnd.minSize = new Vector2(700, 400);
}
// ── 状态 ─────────────────────────────────────────────────────────────
private List<FormSkillSO> _skills = new();
private List<FormSkillSO> _filtered = new();
private ListView _listView;
private VisualElement _detailRoot;
private string _searchText = "";
private string _filterType = "全部";
private InspectorElement _currentInspector;
// ── 生命周期 ──────────────────────────────────────────────────────────
public void CreateGUI()
{
if (_sharedUSS != null)
rootVisualElement.styleSheets.Add(_sharedUSS);
// Toolbar
var toolbar = new Toolbar();
var searchField = new ToolbarSearchField { style = { flexGrow = 1 } };
searchField.RegisterValueChangedCallback(e =>
{
_searchText = e.newValue;
RefreshFilter();
});
toolbar.Add(searchField);
// SkillEffectType 过滤下拉框
var typeFilter = new ToolbarMenu { text = "类型:全部", style = { minWidth = 100 } };
foreach (var opt in _effectTypeOptions)
{
string captured = opt;
typeFilter.menu.AppendAction(opt, _ =>
{
_filterType = captured;
typeFilter.text = $"类型:{captured}";
RefreshFilter();
});
}
toolbar.Add(typeFilter);
var btnCreate = new ToolbarButton(CreateNewSkill) { text = "+ 新建技能" };
var btnRefresh = new ToolbarButton(RefreshAll) { text = "↺" };
btnRefresh.tooltip = "重新扫描 Project 中的 FormSkillSO 资产";
toolbar.Add(btnCreate);
toolbar.Add(btnRefresh);
rootVisualElement.Add(toolbar);
// Split view
var split = new TwoPaneSplitView(0, 220, TwoPaneSplitViewOrientation.Horizontal);
// ── 左栏 ──────────────────────────────────────────────────────
var leftPane = new VisualElement { style = { minWidth = 140 } };
_listView = new ListView
{
selectionType = SelectionType.Single,
fixedItemHeight = 22,
makeItem = MakeListItem,
bindItem = BindListItem,
style = { flexGrow = 1 },
};
_listView.selectionChanged += OnSelectionChanged;
leftPane.Add(_listView);
split.Add(leftPane);
// ── 右栏 ──────────────────────────────────────────────────────
_detailRoot = new ScrollView { style = { flexGrow = 1 } };
_detailRoot.AddToClassList("detail-panel");
split.Add(_detailRoot);
rootVisualElement.Add(split);
RefreshAll();
}
private void OnFocus() => RefreshAll();
// ── 列表构建 ──────────────────────────────────────────────────────────
private void RefreshAll()
{
_skills = EditorScaffoldUtils.FindAllAssetsOfType<FormSkillSO>();
_skills.Sort((a, b) => string.Compare(
a.skillId, b.skillId, StringComparison.OrdinalIgnoreCase));
RefreshFilter();
}
private void RefreshFilter()
{
IEnumerable<FormSkillSO> query = _skills;
if (_filterType != "全部" && Enum.TryParse(_filterType, out SkillEffectType filterEnum))
query = query.Where(s => s.effectType == filterEnum);
if (!string.IsNullOrEmpty(_searchText))
{
string s = _searchText;
query = query.Where(sk => sk != null &&
(sk.skillId?.Contains(s, StringComparison.OrdinalIgnoreCase) == true ||
sk.displayNameKey?.Contains(s, StringComparison.OrdinalIgnoreCase) == true));
}
_filtered = query.ToList();
_listView.itemsSource = _filtered;
_listView.Rebuild();
}
private static VisualElement MakeListItem()
{
var label = new Label();
label.AddToClassList("list-item");
return label;
}
private void BindListItem(VisualElement element, int index)
{
var label = (Label)element;
var skill = _filtered.Count > index ? _filtered[index] : null;
if (skill == null) { label.text = "(null)"; return; }
label.text = string.IsNullOrEmpty(skill.displayNameKey)
? skill.skillId
: $"{skill.skillId} <color=#888>[{skill.effectType}]</color>";
}
// ── 详情面板 ──────────────────────────────────────────────────────────
private void OnSelectionChanged(IEnumerable<object> items)
{
_detailRoot.Clear();
_currentInspector = null;
var skill = items.FirstOrDefault() as FormSkillSO;
if (skill == null) return;
// 标题
var title = new Label($"{skill.skillId} [{skill.effectType}]")
{
style =
{
fontSize = 14,
unityFontStyleAndWeight = FontStyle.Bold,
marginBottom = 6,
}
};
_detailRoot.Add(title);
// 资源消耗快览
BuildCostPreview(skill);
// HitBox Prefab 状态
BuildHitBoxStatus(skill);
// 完整属性编辑
_currentInspector = new InspectorElement(skill);
_detailRoot.Add(_currentInspector);
// 操作按钮
var btnRow = new VisualElement();
btnRow.AddToClassList("action-buttons");
btnRow.Add(new Button(() => EditorScaffoldUtils.PingAndSelect(skill))
{ text = "在 Project 中定位" });
btnRow.Add(new Button(() => Selection.activeObject = skill)
{ text = "在 Inspector 中打开" });
btnRow.Add(new Button(SkillHitBoxWizard.Open)
{ text = "HitBox Prefab 向导…" });
_detailRoot.Add(btnRow);
}
private void BuildCostPreview(FormSkillSO skill)
{
var box = new VisualElement();
box.AddToClassList("stats-preview");
void AddStat(string label, string value)
{
box.Add(new Label(label) { style = { color = new Color(0.7f, 0.7f, 0.7f), marginRight = 4 } });
box.Add(new Label(value) { style = { marginRight = 16, unityFontStyleAndWeight = FontStyle.Bold } });
}
AddStat("消耗:", $"{skill.baseCost} {skill.resourceType}");
AddStat("冷却:", $"{skill.cooldown:F1}s");
AddStat("施放锁:", $"{skill.castLockDuration:F2}s");
_detailRoot.Add(box);
}
private void BuildHitBoxStatus(FormSkillSO skill)
{
// 投射物技能不需要近战 HitBox Prefab
if (skill.effectType == SkillEffectType.Projectile) return;
HelpBoxMessageType msgType;
string msg;
if (skill.SkillHitBoxPrefab == null)
{
msgType = HelpBoxMessageType.Warning;
msg = "SkillHitBoxPrefab 未赋值!近战/爆炸技能需要关联 HitBox Prefab。";
}
else if (skill.SkillHitBoxPrefab.GetComponent<SkillHitBoxInstance>() == null)
{
msgType = HelpBoxMessageType.Error;
msg = $"SkillHitBoxPrefab「{skill.SkillHitBoxPrefab.name}」缺少 SkillHitBoxInstance 组件!";
}
else
{
msgType = HelpBoxMessageType.Info;
msg = $"HitBox Prefab 结构正常:{skill.SkillHitBoxPrefab.name}";
}
_detailRoot.Add(new HelpBox(msg, msgType) { style = { marginBottom = 6 } });
}
// ── 新建技能 ──────────────────────────────────────────────────────────
private void CreateNewSkill()
{
var asset = EditorScaffoldUtils.CreateSOAsset<FormSkillSO>(
"Assets/_Game/Data/Skills", "FormSkillSO_New");
if (asset != null)
{
RefreshAll();
int idx = _filtered.IndexOf(asset);
if (idx >= 0)
_listView.SetSelection(idx);
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 22de97a32c867fd429c1814853d61ec6
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,127 @@
using System.IO;
using UnityEditor;
using UnityEngine;
using BaseGames.Combat;
namespace BaseGames.Editor.Skills
{
/// <summary>
/// 向导:一键生成技能 HitBox PrefabW-11
/// 菜单BaseGames / Create / Skill HitBox Prefab
///
/// 生成路径规范Assets/_Game/Prefabs/Skills/SKL_{skillId}_HitBox.prefab
/// Prefab 结构(以 hitBoxCount=1 为例):
/// [SKL_{skillId}_HitBox] ← SkillHitBoxInstance (_hitBoxes 自动赋值)
/// └── [HitBox_0] ← PolygonCollider2D(IsTrigger) + HitBox, Layer=PlayerHitBox
/// </summary>
public class SkillHitBoxWizard : ScriptableWizard
{
private const string OutputFolder = "Assets/_Game/Prefabs/Skills";
[MenuItem("BaseGames/Create/Skill HitBox Prefab", priority = 201)]
public static void Open() =>
DisplayWizard<SkillHitBoxWizard>("Skill HitBox Prefab 向导", "创建");
[Tooltip("技能唯一 ID如 SkySlash。Prefab 将命名为 SKL_{skillId}_HitBox")]
public string skillId = "";
[Tooltip("多段伤害时可设置 >1每段一个 HitBox 子节点)")]
[Range(1, 4)]
public int hitBoxCount = 1;
// ── 向导回调 ──────────────────────────────────────────────────────────
private void OnWizardUpdate()
{
isValid = !string.IsNullOrWhiteSpace(skillId);
helpString = isValid
? $"将创建:{OutputFolder}/SKL_{skillId}_HitBox.prefab{hitBoxCount} 个 HitBox"
: "请输入 skillId技能唯一 ID如 SkySlash。";
}
private void OnWizardCreate()
{
if (string.IsNullOrWhiteSpace(skillId))
{
EditorUtility.DisplayDialog("错误", "skillId 不能为空。", "确认");
return;
}
string prefabName = $"SKL_{skillId}_HitBox";
string assetPath = $"{OutputFolder}/{prefabName}.prefab";
string fullPath = Path.Combine(
Path.GetDirectoryName(Application.dataPath)!,
assetPath.Replace('/', Path.DirectorySeparatorChar));
if (File.Exists(fullPath))
{
if (!EditorUtility.DisplayDialog("已存在",
$"{assetPath}\n\n该 Prefab 已存在,是否覆盖?",
"覆盖", "取消"))
return;
}
EditorScaffoldUtils.EnsureFolder(OutputFolder);
int hitBoxLayer = LayerMask.NameToLayer("PlayerHitBox");
if (hitBoxLayer < 0)
{
Debug.LogWarning("[SkillHitBoxWizard] 未找到 Physics Layer 'PlayerHitBox',子节点 Layer 将设为 Default。");
hitBoxLayer = 0;
}
// ── 构建 Prefab ────────────────────────────────────────────────
var root = new GameObject(prefabName);
var instance = root.AddComponent<SkillHitBoxInstance>();
var so = new SerializedObject(instance);
var hbRefs = new HitBox[hitBoxCount];
for (int i = 0; i < hitBoxCount; i++)
{
var child = new GameObject($"HitBox_{i}");
child.transform.SetParent(root.transform, false);
child.layer = hitBoxLayer;
// PolygonCollider2D 默认菱形1×0.5),策划在 Scene 中调整具体形状
var poly = child.AddComponent<PolygonCollider2D>();
poly.isTrigger = true;
poly.SetPath(0, new Vector2[]
{
new(-0.5f, -0.25f), new(0.5f, -0.25f),
new(0.5f, 0.25f), new(-0.5f, 0.25f),
});
hbRefs[i] = child.AddComponent<HitBox>();
}
// 将 HitBox[] 赋值给 _hitBoxes
var arrayProp = so.FindProperty("_hitBoxes");
if (arrayProp != null && arrayProp.isArray)
{
arrayProp.arraySize = hitBoxCount;
for (int i = 0; i < hitBoxCount; i++)
arrayProp.GetArrayElementAtIndex(i).objectReferenceValue = hbRefs[i];
so.ApplyModifiedPropertiesWithoutUndo();
}
else
{
Debug.LogWarning("[SkillHitBoxWizard] 未找到 SkillHitBoxInstance._hitBoxes 字段,请手动赋值。");
}
var prefab = PrefabUtility.SaveAsPrefabAsset(root, assetPath);
Object.DestroyImmediate(root);
AssetDatabase.Refresh();
if (prefab != null)
{
EditorScaffoldUtils.PingAndSelect(prefab);
Debug.Log($"[SkillHitBoxWizard] 已创建:{assetPath}");
}
else
{
Debug.LogError($"[SkillHitBoxWizard] Prefab 保存失败:{assetPath}");
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 20fe05f2dec972d4093ff334f845dc0f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: