摄像机区域的架构改动
This commit is contained in:
157
Assets/_Game/Scripts/Editor/Combat/DamageSourceInspector.cs
Normal file
157
Assets/_Game/Scripts/Editor/Combat/DamageSourceInspector.cs
Normal file
@@ -0,0 +1,157 @@
|
||||
using System;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using BaseGames.Combat;
|
||||
|
||||
namespace BaseGames.Editor.Combat
|
||||
{
|
||||
/// <summary>
|
||||
/// DamageSourceSO 增强 Inspector(W-09)。
|
||||
/// 在默认字段下方追加:
|
||||
/// ① 伤害预览(BaseDamage × DamageMultiplier)
|
||||
/// ② BreakLevel 颜色标签
|
||||
/// ③ DamageFlags / DamageTags 逐位 Toggle 组(可编辑,替代默认位掩码数字)
|
||||
/// </summary>
|
||||
[CustomEditor(typeof(DamageSourceSO))]
|
||||
public class DamageSourceInspector : UnityEditor.Editor
|
||||
{
|
||||
private static readonly Color[] _breakColors =
|
||||
{
|
||||
new Color(0.55f, 0.55f, 0.55f), // None — 灰
|
||||
new Color(0.25f, 0.80f, 0.25f), // Light — 绿
|
||||
new Color(0.25f, 0.55f, 0.95f), // Medium — 蓝
|
||||
new Color(1.00f, 0.60f, 0.10f), // Heavy — 橙
|
||||
new Color(0.90f, 0.15f, 0.15f), // Breaker — 红
|
||||
};
|
||||
|
||||
private static readonly string[] _breakLabels = { "None", "Light", "Medium", "Heavy", "Breaker" };
|
||||
|
||||
// ── DamageFlags 枚举值(排除 None=0)
|
||||
private static readonly DamageFlags[] _allFlags =
|
||||
(DamageFlags[])Enum.GetValues(typeof(DamageFlags));
|
||||
|
||||
// ── DamageTags 枚举值(排除 None=0)
|
||||
private static readonly DamageTags[] _allTags =
|
||||
(DamageTags[])Enum.GetValues(typeof(DamageTags));
|
||||
|
||||
public override void OnInspectorGUI()
|
||||
{
|
||||
DrawDefaultInspector();
|
||||
|
||||
var src = (DamageSourceSO)target;
|
||||
|
||||
EditorGUILayout.Space(10);
|
||||
|
||||
// ── ① 伤害预览 ────────────────────────────────────────────────────
|
||||
EditorGUILayout.LabelField("── 伤害预览", EditorStyles.boldLabel);
|
||||
|
||||
using (new EditorGUI.DisabledGroupScope(true))
|
||||
{
|
||||
int calculated = Mathf.RoundToInt(src.BaseDamage * src.DamageMultiplier);
|
||||
EditorGUILayout.IntField(
|
||||
new GUIContent("计算伤害 (Base × Multi)", "= BaseDamage × DamageMultiplier,四舍五入"),
|
||||
calculated);
|
||||
}
|
||||
|
||||
// ── ② BreakLevel 颜色标签 ─────────────────────────────────────────
|
||||
int idx = Mathf.Clamp((int)src.BreakLevel, 0, _breakColors.Length - 1);
|
||||
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
EditorGUILayout.PrefixLabel(new GUIContent("破霸体等级(颜色预览)"));
|
||||
|
||||
var colorStyle = new GUIStyle(EditorStyles.helpBox)
|
||||
{
|
||||
alignment = TextAnchor.MiddleCenter,
|
||||
fontStyle = FontStyle.Bold,
|
||||
normal = { textColor = Color.white },
|
||||
};
|
||||
Color prev = GUI.backgroundColor;
|
||||
GUI.backgroundColor = _breakColors[idx];
|
||||
GUILayout.Box(_breakLabels[idx], colorStyle, GUILayout.Width(72), GUILayout.Height(18));
|
||||
GUI.backgroundColor = prev;
|
||||
EditorGUILayout.EndHorizontal();
|
||||
|
||||
EditorGUILayout.Space(8);
|
||||
|
||||
// ── ③ DamageFlags Toggle 组 ───────────────────────────────────────
|
||||
EditorGUILayout.LabelField("── DamageFlags(位标志,可直接勾选)", EditorStyles.boldLabel);
|
||||
|
||||
var flagsProp = serializedObject.FindProperty("Flags");
|
||||
serializedObject.Update();
|
||||
int flagsVal = flagsProp.intValue;
|
||||
bool flagsChanged = false;
|
||||
|
||||
var validFlags = System.Array.FindAll(_allFlags, f => f != DamageFlags.None);
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
int col = 0;
|
||||
for (int fi = 0; fi < validFlags.Length; fi++)
|
||||
{
|
||||
var flag = validFlags[fi];
|
||||
bool wasSet = (flagsVal & (int)flag) != 0;
|
||||
bool nowSet = GUILayout.Toggle(wasSet, flag.ToString(),
|
||||
GUI.skin.button, GUILayout.Height(18), GUILayout.MinWidth(90));
|
||||
if (nowSet != wasSet)
|
||||
{
|
||||
flagsVal = nowSet ? flagsVal | (int)flag : flagsVal & ~(int)flag;
|
||||
flagsChanged = true;
|
||||
}
|
||||
col++;
|
||||
if (col % 4 == 0 && fi < validFlags.Length - 1)
|
||||
{
|
||||
EditorGUILayout.EndHorizontal();
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
}
|
||||
}
|
||||
EditorGUILayout.EndHorizontal();
|
||||
|
||||
if (flagsChanged)
|
||||
{
|
||||
flagsProp.intValue = flagsVal;
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
}
|
||||
|
||||
// 不安全 Flags 警告
|
||||
if ((src.Flags & DamageFlags.Unblockable) != 0)
|
||||
EditorGUILayout.HelpBox("Unblockable:玩家无法格挡此伤害,请确认设计意图。", MessageType.Warning);
|
||||
|
||||
EditorGUILayout.Space(6);
|
||||
|
||||
// ── ④ DamageTags Toggle 组 ────────────────────────────────────────
|
||||
EditorGUILayout.LabelField("── DamageTags(交互标签,可直接勾选)", EditorStyles.boldLabel);
|
||||
|
||||
var tagsProp = serializedObject.FindProperty("Tags");
|
||||
serializedObject.Update();
|
||||
uint tagsVal = (uint)tagsProp.longValue;
|
||||
bool tagsChanged = false;
|
||||
|
||||
var validTags = System.Array.FindAll(_allTags, t => t != DamageTags.None);
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
col = 0;
|
||||
for (int ti = 0; ti < validTags.Length; ti++)
|
||||
{
|
||||
var tag = validTags[ti];
|
||||
bool wasSet = (tagsVal & (uint)tag) != 0;
|
||||
bool nowSet = GUILayout.Toggle(wasSet, tag.ToString(),
|
||||
GUI.skin.button, GUILayout.Height(18), GUILayout.MinWidth(90));
|
||||
if (nowSet != wasSet)
|
||||
{
|
||||
tagsVal = nowSet ? tagsVal | (uint)tag : tagsVal & ~(uint)tag;
|
||||
tagsChanged = true;
|
||||
}
|
||||
col++;
|
||||
if (col % 4 == 0 && ti < validTags.Length - 1)
|
||||
{
|
||||
EditorGUILayout.EndHorizontal();
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
}
|
||||
}
|
||||
EditorGUILayout.EndHorizontal();
|
||||
|
||||
if (tagsChanged)
|
||||
{
|
||||
tagsProp.longValue = tagsVal;
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2562d64f9a5ca764981b69d3c62d1c17
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
64
Assets/_Game/Scripts/Editor/Combat/HurtBoxEditor.cs
Normal file
64
Assets/_Game/Scripts/Editor/Combat/HurtBoxEditor.cs
Normal file
@@ -0,0 +1,64 @@
|
||||
#if UNITY_EDITOR
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using BaseGames.Combat;
|
||||
|
||||
namespace BaseGames.Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// HurtBox 运行时注入状态可视化面板。
|
||||
/// 通过 HurtBox 上的 Editor* 属性读取注入状态,以颜色区分是否注入成功。
|
||||
/// 绿色 = 注入完成;橙色 = 未注入(该能力静默不生效);灰色 = 非 PlayMode。
|
||||
/// </summary>
|
||||
[CustomEditor(typeof(HurtBox))]
|
||||
public class HurtBoxEditor : UnityEditor.Editor
|
||||
{
|
||||
// (属性访问器, 标签, 缺席说明)
|
||||
private static readonly (System.Func<HurtBox, object> getter, string label, string absentNote)[] _fields =
|
||||
{
|
||||
(hb => hb.EditorOwner, "Owner (IDamageable)", "— 注入失败,ReceiveDamage 将无效"),
|
||||
(hb => hb.EditorShieldable, "Shieldable", "— 未注入(玩家专属,敌人无需)"),
|
||||
(hb => hb.EditorParrySystem, "ParrySystem", "— 未注入(弹反静默不生效)"),
|
||||
(hb => hb.EditorPoiseSource, "PoiseSource", "— 未注入(霸体静默不生效)"),
|
||||
(hb => hb.EditorStatusEffectable, "StatusEffectable", "— 未注入(状态效果静默不生效)"),
|
||||
};
|
||||
|
||||
public override void OnInspectorGUI()
|
||||
{
|
||||
DrawDefaultInspector();
|
||||
|
||||
EditorGUILayout.Space(4);
|
||||
EditorGUILayout.LabelField("── 运行时注入状态 ──", EditorStyles.boldLabel);
|
||||
|
||||
if (!Application.isPlaying)
|
||||
{
|
||||
EditorGUILayout.HelpBox("进入 PlayMode 后查看注入状态。", MessageType.Info);
|
||||
return;
|
||||
}
|
||||
|
||||
var hurtBox = (HurtBox)target;
|
||||
|
||||
foreach (var (getter, label, absentNote) in _fields)
|
||||
{
|
||||
var value = getter(hurtBox);
|
||||
bool present = value != null;
|
||||
|
||||
var savedColor = GUI.contentColor;
|
||||
GUI.contentColor = present
|
||||
? new Color(0.3f, 0.9f, 0.4f) // 绿
|
||||
: new Color(1.0f, 0.6f, 0.1f); // 橙
|
||||
|
||||
string displayValue = present
|
||||
? $"✓ {value.GetType().Name}"
|
||||
: $"✗ null {absentNote}";
|
||||
|
||||
EditorGUILayout.LabelField(label, displayValue);
|
||||
GUI.contentColor = savedColor;
|
||||
}
|
||||
|
||||
// 持续刷新(避免只显示初始状态)
|
||||
if (Application.isPlaying) Repaint();
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
11
Assets/_Game/Scripts/Editor/Combat/HurtBoxEditor.cs.meta
Normal file
11
Assets/_Game/Scripts/Editor/Combat/HurtBoxEditor.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8650ccc7960fe304a95be1c629ef7b1e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
339
Assets/_Game/Scripts/Editor/Combat/WeaponEditorWindow.cs
Normal file
339
Assets/_Game/Scripts/Editor/Combat/WeaponEditorWindow.cs
Normal file
@@ -0,0 +1,339 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Animancer;
|
||||
using UnityEditor;
|
||||
using UnityEditor.UIElements;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
using BaseGames.Combat;
|
||||
using BaseGames.Player;
|
||||
|
||||
namespace BaseGames.Editor.Combat
|
||||
{
|
||||
/// <summary>
|
||||
/// 武器数据管理窗口(W-02)。
|
||||
/// 技术:UI Toolkit TwoPaneSplitView。
|
||||
/// 菜单:BaseGames / Data / Weapon Editor
|
||||
///
|
||||
/// 左栏:可搜索的 WeaponSO 列表 + [新建] 按钮。
|
||||
/// 右栏:选中武器的完整属性编辑 + HitBox Prefab 结构校验 + 快速操作。
|
||||
/// </summary>
|
||||
public class WeaponEditorWindow : EditorWindow
|
||||
{
|
||||
private static readonly StyleSheet _sharedUSS;
|
||||
|
||||
static WeaponEditorWindow()
|
||||
{
|
||||
_sharedUSS = AssetDatabase.LoadAssetAtPath<StyleSheet>(
|
||||
"Assets/_Game/Scripts/Editor/UIToolkit/Editor.uss");
|
||||
}
|
||||
|
||||
[MenuItem("BaseGames/Data/Weapon Editor", priority = 100)]
|
||||
public static void Open()
|
||||
{
|
||||
var wnd = GetWindow<WeaponEditorWindow>();
|
||||
wnd.titleContent = new GUIContent("Weapon Editor");
|
||||
wnd.minSize = new Vector2(680, 400);
|
||||
}
|
||||
|
||||
// ── 状态 ─────────────────────────────────────────────────────────────
|
||||
private List<WeaponSO> _weapons = new();
|
||||
private List<WeaponSO> _filtered = new();
|
||||
private ListView _listView;
|
||||
private VisualElement _detailRoot;
|
||||
private string _searchText = "";
|
||||
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);
|
||||
|
||||
var btnCreate = new ToolbarButton(CreateNewWeapon) { text = "+ 新建武器" };
|
||||
toolbar.Add(btnCreate);
|
||||
|
||||
var btnRefresh = new ToolbarButton(RefreshAll) { text = "↺" };
|
||||
btnRefresh.tooltip = "重新扫描 Project 中的 WeaponSO 资产";
|
||||
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()
|
||||
{
|
||||
_weapons = EditorScaffoldUtils.FindAllAssetsOfType<WeaponSO>();
|
||||
_weapons.Sort((a, b) => string.Compare(
|
||||
a.weaponId, b.weaponId, StringComparison.OrdinalIgnoreCase));
|
||||
RefreshFilter();
|
||||
}
|
||||
|
||||
private void RefreshFilter()
|
||||
{
|
||||
if (string.IsNullOrEmpty(_searchText))
|
||||
{
|
||||
_filtered = new List<WeaponSO>(_weapons);
|
||||
}
|
||||
else
|
||||
{
|
||||
string s = _searchText;
|
||||
_filtered = _weapons.Where(w => w != null &&
|
||||
(w.weaponId?.Contains(s, StringComparison.OrdinalIgnoreCase) == true ||
|
||||
w.displayName?.Contains(s, StringComparison.OrdinalIgnoreCase) == true)).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 weapon = _filtered.Count > index ? _filtered[index] : null;
|
||||
if (weapon == null) { label.text = "(null)"; return; }
|
||||
|
||||
label.text = string.IsNullOrEmpty(weapon.displayName)
|
||||
? weapon.weaponId
|
||||
: $"{weapon.weaponId} <color=#888>({weapon.displayName})</color>";
|
||||
}
|
||||
|
||||
// ── 详情面板 ──────────────────────────────────────────────────────────
|
||||
|
||||
private void OnSelectionChanged(IEnumerable<object> items)
|
||||
{
|
||||
_detailRoot.Clear();
|
||||
_currentInspector = null;
|
||||
|
||||
var weapon = items.FirstOrDefault() as WeaponSO;
|
||||
if (weapon == null) return;
|
||||
|
||||
// 标题
|
||||
var title = new Label(
|
||||
string.IsNullOrEmpty(weapon.displayName) ? weapon.weaponId : $"{weapon.weaponId} · {weapon.displayName}")
|
||||
{
|
||||
style =
|
||||
{
|
||||
fontSize = 14,
|
||||
unityFontStyleAndWeight = FontStyle.Bold,
|
||||
marginBottom = 6,
|
||||
}
|
||||
};
|
||||
_detailRoot.Add(title);
|
||||
|
||||
// HitBox Prefab 状态
|
||||
BuildHitBoxStatus(weapon);
|
||||
|
||||
// 连击链预览
|
||||
BuildComboPreview(weapon);
|
||||
|
||||
// Inspector 完整属性编辑
|
||||
_currentInspector = new InspectorElement(weapon);
|
||||
_detailRoot.Add(_currentInspector);
|
||||
|
||||
// 操作按钮
|
||||
var btnRow = new VisualElement();
|
||||
btnRow.AddToClassList("action-buttons");
|
||||
|
||||
var btnSelect = new Button(() => EditorScaffoldUtils.PingAndSelect(weapon))
|
||||
{ text = "在 Project 中定位" };
|
||||
var btnInspector = new Button(() => Selection.activeObject = weapon)
|
||||
{ text = "在 Inspector 中打开" };
|
||||
var btnWizard = new Button(WeaponHitBoxWizard.Open)
|
||||
{ text = "HitBox Prefab 向导…" };
|
||||
|
||||
btnRow.Add(btnSelect);
|
||||
btnRow.Add(btnInspector);
|
||||
btnRow.Add(btnWizard);
|
||||
_detailRoot.Add(btnRow);
|
||||
}
|
||||
|
||||
/// <summary>attack1 → attack2 → attack3 连击链数值横排预览。</summary>
|
||||
private void BuildComboPreview(WeaponSO weapon)
|
||||
{
|
||||
// 只在有至少一个连击数据时显示
|
||||
if (weapon.attack1Source == null && weapon.attack2Source == null && weapon.attack3Source == null)
|
||||
return;
|
||||
|
||||
var section = new Label("连击链预览") { style = { unityFontStyleAndWeight = FontStyle.Bold, marginBottom = 4 } };
|
||||
_detailRoot.Add(section);
|
||||
|
||||
var chain = new VisualElement();
|
||||
chain.AddToClassList("stats-preview");
|
||||
|
||||
void AddSegment(string label, ClipTransition clip, DamageSourceSO src, bool addArrow)
|
||||
{
|
||||
var cell = new VisualElement
|
||||
{
|
||||
style =
|
||||
{
|
||||
alignItems = Align.Center,
|
||||
marginRight = 4,
|
||||
paddingLeft = 6,
|
||||
paddingRight = 6,
|
||||
paddingTop = 3,
|
||||
paddingBottom = 3,
|
||||
backgroundColor = new Color(0.25f, 0.25f, 0.28f, 1f),
|
||||
borderTopLeftRadius = 3,
|
||||
borderTopRightRadius = 3,
|
||||
borderBottomLeftRadius = 3,
|
||||
borderBottomRightRadius = 3,
|
||||
}
|
||||
};
|
||||
|
||||
// 段名
|
||||
cell.Add(new Label(label)
|
||||
{
|
||||
style = { fontSize = 10, color = new Color(0.65f, 0.65f, 0.65f) }
|
||||
});
|
||||
|
||||
// Clip 名称
|
||||
string clipName = clip?.Clip != null ? clip.Clip.name : "<无动画>";
|
||||
cell.Add(new Label(clipName)
|
||||
{
|
||||
style = { fontSize = 11, unityFontStyleAndWeight = FontStyle.Bold }
|
||||
});
|
||||
|
||||
// 伤害数值
|
||||
if (src != null)
|
||||
{
|
||||
int dmg = Mathf.RoundToInt(src.BaseDamage * src.DamageMultiplier);
|
||||
cell.Add(new Label($"伤害 {dmg} [{src.BreakLevel}]")
|
||||
{
|
||||
style = { fontSize = 10, color = new Color(1f, 0.7f, 0.3f) }
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
cell.Add(new Label("(无 DamageSource)")
|
||||
{
|
||||
style = { fontSize = 10, color = new Color(0.8f, 0.3f, 0.3f) }
|
||||
});
|
||||
}
|
||||
|
||||
chain.Add(cell);
|
||||
|
||||
if (addArrow)
|
||||
chain.Add(new Label("→") { style = { alignSelf = Align.Center, marginLeft = 2, marginRight = 2 } });
|
||||
}
|
||||
|
||||
AddSegment("攻击1", weapon.attack1Clip, weapon.attack1Source, true);
|
||||
AddSegment("攻击2", weapon.attack2Clip, weapon.attack2Source, true);
|
||||
AddSegment("攻击3", weapon.attack3Clip, weapon.attack3Source, false);
|
||||
|
||||
_detailRoot.Add(chain);
|
||||
|
||||
// 追加空中/上/下攻击的简要行
|
||||
var extraRow = new VisualElement
|
||||
{
|
||||
style = { flexDirection = FlexDirection.Row, flexWrap = Wrap.Wrap, marginBottom = 6, paddingLeft = 6 }
|
||||
};
|
||||
|
||||
void ExtraStat(string label, DamageSourceSO src)
|
||||
{
|
||||
if (src == null) return;
|
||||
int dmg = Mathf.RoundToInt(src.BaseDamage * src.DamageMultiplier);
|
||||
extraRow.Add(new Label($"{label}:{dmg} [{src.BreakLevel}]")
|
||||
{
|
||||
style = { marginRight = 14, fontSize = 11, color = new Color(0.7f, 0.7f, 0.7f) }
|
||||
});
|
||||
}
|
||||
|
||||
ExtraStat("空中", weapon.airAttackSource);
|
||||
ExtraStat("上挑", weapon.upAttackSource);
|
||||
ExtraStat("下砸", weapon.downAttackSource);
|
||||
|
||||
if (extraRow.childCount > 0)
|
||||
_detailRoot.Add(extraRow);
|
||||
}
|
||||
|
||||
private void BuildHitBoxStatus(WeaponSO weapon)
|
||||
{
|
||||
HelpBoxMessageType msgType;
|
||||
string msg;
|
||||
|
||||
if (weapon.hitBoxPrefab == null)
|
||||
{
|
||||
msgType = HelpBoxMessageType.Warning;
|
||||
msg = "hitBoxPrefab 未赋值!请创建并关联武器 HitBox Prefab。";
|
||||
}
|
||||
else if (weapon.hitBoxPrefab.GetComponent<WeaponHitBoxInstance>() == null)
|
||||
{
|
||||
msgType = HelpBoxMessageType.Error;
|
||||
msg = $"hitBoxPrefab「{weapon.hitBoxPrefab.name}」缺少 WeaponHitBoxInstance 组件!";
|
||||
}
|
||||
else
|
||||
{
|
||||
msgType = HelpBoxMessageType.Info;
|
||||
msg = $"HitBox Prefab 结构正常:{weapon.hitBoxPrefab.name}";
|
||||
}
|
||||
|
||||
_detailRoot.Add(new HelpBox(msg, msgType) { style = { marginBottom = 6 } });
|
||||
}
|
||||
|
||||
// ── 新建武器 ──────────────────────────────────────────────────────────
|
||||
|
||||
private void CreateNewWeapon()
|
||||
{
|
||||
var asset = EditorScaffoldUtils.CreateSOAsset<WeaponSO>(
|
||||
"Assets/_Game/Data/Player/Weapons", "WeaponSO_New");
|
||||
|
||||
if (asset != null)
|
||||
{
|
||||
RefreshAll();
|
||||
int idx = _filtered.IndexOf(asset);
|
||||
if (idx >= 0)
|
||||
_listView.SetSelection(idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7cc9d2828e2d3f9458e74befbb0e2b4e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
181
Assets/_Game/Scripts/Editor/Combat/WeaponHitBoxWizard.cs
Normal file
181
Assets/_Game/Scripts/Editor/Combat/WeaponHitBoxWizard.cs
Normal file
@@ -0,0 +1,181 @@
|
||||
using System.IO;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using BaseGames.Combat;
|
||||
using BaseGames.Player;
|
||||
|
||||
namespace BaseGames.Editor.Combat
|
||||
{
|
||||
/// <summary>
|
||||
/// 向导:一键生成武器 HitBox Prefab(W-10)。
|
||||
/// 菜单:BaseGames / Create / Weapon HitBox Prefab
|
||||
///
|
||||
/// 生成路径规范:Assets/_Game/Prefabs/Weapons/WPN_{weaponId}_HitBox.prefab
|
||||
/// Prefab 结构:
|
||||
/// [WPN_{weaponId}_HitBox] ← WeaponHitBoxInstance
|
||||
/// ├── [HitBox_Ground] ← Collider2D(IsTrigger, 形状可选) + HitBox, Layer=PlayerHitBox
|
||||
/// ├── [HitBox_Up]
|
||||
/// ├── [HitBox_Down]
|
||||
/// └── [HitBox_Air]
|
||||
/// </summary>
|
||||
public class WeaponHitBoxWizard : ScriptableWizard
|
||||
{
|
||||
private const string OutputFolder = "Assets/_Game/Prefabs/Weapons";
|
||||
|
||||
/// <summary>每个方向可选的 Collider 形状。</summary>
|
||||
public enum ColliderShape
|
||||
{
|
||||
[Tooltip("BoxCollider2D — 矩形,适合水平/垂直扫击")]
|
||||
Box,
|
||||
[Tooltip("CapsuleCollider2D — 胶囊体,适合刺击或弧形")]
|
||||
Capsule,
|
||||
[Tooltip("PolygonCollider2D(菱形默认点)— 适合不规则斩击")]
|
||||
Polygon,
|
||||
}
|
||||
|
||||
[MenuItem("BaseGames/Create/Weapon HitBox Prefab", priority = 200)]
|
||||
public static void Open() =>
|
||||
DisplayWizard<WeaponHitBoxWizard>("Weapon HitBox Prefab 向导", "创建");
|
||||
|
||||
[Tooltip("武器唯一 ID,如 SkyBlade。Prefab 将命名为 WPN_{weaponId}_HitBox")]
|
||||
public string weaponId = "";
|
||||
|
||||
[Header("包含哪些攻击方向")]
|
||||
public bool includeGround = true;
|
||||
public bool includeUp = true;
|
||||
public bool includeDown = true;
|
||||
public bool includeAir = true;
|
||||
|
||||
[Header("每个方向的 Collider 形状")]
|
||||
[Tooltip("Ground / 落地攻击的碰撞体形状")]
|
||||
public ColliderShape groundShape = ColliderShape.Box;
|
||||
[Tooltip("Up / 上挑攻击的碰撞体形状")]
|
||||
public ColliderShape upShape = ColliderShape.Capsule;
|
||||
[Tooltip("Down / 下砸攻击的碰撞体形状")]
|
||||
public ColliderShape downShape = ColliderShape.Box;
|
||||
[Tooltip("Air / 空中攻击的碰撞体形状")]
|
||||
public ColliderShape airShape = ColliderShape.Capsule;
|
||||
|
||||
// ── 向导回调 ──────────────────────────────────────────────────────────
|
||||
|
||||
private void OnWizardUpdate()
|
||||
{
|
||||
isValid = !string.IsNullOrWhiteSpace(weaponId);
|
||||
helpString = isValid
|
||||
? $"将创建:{OutputFolder}/WPN_{weaponId}_HitBox.prefab"
|
||||
: "请输入 weaponId(武器唯一 ID,如 SkyBlade)。";
|
||||
}
|
||||
|
||||
private void OnWizardCreate()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(weaponId))
|
||||
{
|
||||
EditorUtility.DisplayDialog("错误", "weaponId 不能为空。", "确认");
|
||||
return;
|
||||
}
|
||||
|
||||
string prefabName = $"WPN_{weaponId}_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("[WeaponHitBoxWizard] 未找到 Physics Layer 'PlayerHitBox',子节点 Layer 将设为 Default。");
|
||||
hitBoxLayer = 0;
|
||||
}
|
||||
|
||||
// ── 构建 Prefab ────────────────────────────────────────────────
|
||||
var root = new GameObject(prefabName);
|
||||
var instance = root.AddComponent<WeaponHitBoxInstance>();
|
||||
var so = new SerializedObject(instance);
|
||||
|
||||
void AddDirection(bool enabled, string nodeName, string fieldName, ColliderShape shape)
|
||||
{
|
||||
if (!enabled) return;
|
||||
|
||||
var child = new GameObject(nodeName);
|
||||
child.transform.SetParent(root.transform, false);
|
||||
child.layer = hitBoxLayer;
|
||||
|
||||
AddCollider(child, shape);
|
||||
|
||||
var hb = child.AddComponent<HitBox>();
|
||||
var prop = so.FindProperty(fieldName);
|
||||
if (prop != null)
|
||||
prop.objectReferenceValue = hb;
|
||||
}
|
||||
|
||||
AddDirection(includeGround, "HitBox_Ground", "_hitBoxGround", groundShape);
|
||||
AddDirection(includeUp, "HitBox_Up", "_hitBoxUp", upShape);
|
||||
AddDirection(includeDown, "HitBox_Down", "_hitBoxDown", downShape);
|
||||
AddDirection(includeAir, "HitBox_Air", "_hitBoxAir", airShape);
|
||||
|
||||
so.ApplyModifiedPropertiesWithoutUndo();
|
||||
|
||||
var prefab = PrefabUtility.SaveAsPrefabAsset(root, assetPath);
|
||||
Object.DestroyImmediate(root);
|
||||
AssetDatabase.Refresh();
|
||||
|
||||
if (prefab != null)
|
||||
{
|
||||
EditorScaffoldUtils.PingAndSelect(prefab);
|
||||
Debug.Log($"[WeaponHitBoxWizard] 已创建:{assetPath}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError($"[WeaponHitBoxWizard] Prefab 保存失败:{assetPath}");
|
||||
}
|
||||
}
|
||||
|
||||
// ── 辅助:按形状添加 2D 碰撞体 ────────────────────────────────────────
|
||||
|
||||
private static void AddCollider(GameObject go, ColliderShape shape)
|
||||
{
|
||||
switch (shape)
|
||||
{
|
||||
case ColliderShape.Box:
|
||||
{
|
||||
var c = go.AddComponent<BoxCollider2D>();
|
||||
c.isTrigger = true;
|
||||
c.size = new Vector2(1f, 0.5f);
|
||||
break;
|
||||
}
|
||||
case ColliderShape.Capsule:
|
||||
{
|
||||
var c = go.AddComponent<CapsuleCollider2D>();
|
||||
c.isTrigger = true;
|
||||
c.size = new Vector2(0.5f, 1f);
|
||||
break;
|
||||
}
|
||||
case ColliderShape.Polygon:
|
||||
{
|
||||
var c = go.AddComponent<PolygonCollider2D>();
|
||||
c.isTrigger = true;
|
||||
// 默认菱形点(0.5 × 0.5 单位)
|
||||
c.SetPath(0, new Vector2[]
|
||||
{
|
||||
new( 0f, 0.3f),
|
||||
new( 0.5f, 0f ),
|
||||
new( 0f, -0.3f),
|
||||
new(-0.5f, 0f ),
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 85778ca4e33d8d441abe05d60450d1ab
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user