using System; using System.Collections.Generic; using Cielonos.MainGame.Characters; using Lean.Pool; using Sirenix.OdinInspector; using SoftCircuits.Collections; using UnityEngine; using UnityEngine.Serialization; namespace Cielonos.MainGame { [CreateAssetMenu(fileName = "VFXData", menuName = "Cielonos/VFXData")] public partial class VFXData : SerializedScriptableObject { [DictionaryDrawerSettings(KeyLabel = "VFX Name", DisplayMode = DictionaryDisplayOptions.ExpandedFoldout)] [Searchable] public OrderedDictionary collection = new OrderedDictionary(); public VFXUnit Get(string effectName) => collection[effectName]; } public partial class VFXData { //Runtime [NonSerialized] private CharacterBase executor; [NonSerialized] private Transform executorTransform; public void Initialize(CharacterBase character) { executor = character; executorTransform = character.transform; } public GameObject SpawnVFX(string vfxName, Transform overrideStartTransform = null) { VFXUnit vfxUnit = Get(vfxName); Transform startTransform = overrideStartTransform ?? executorTransform; GameObject vfxInstance = VFXObject.Spawn(vfxUnit.mainVFX, executor, startTransform); Transform vfxTransform = vfxInstance.transform; vfxTransform.localPosition = vfxUnit.localPosition; vfxTransform.localEulerAngles = vfxUnit.localEulerAngles; vfxTransform.localScale = vfxUnit.localScale; if (!vfxUnit.keepLocalTransform) { vfxTransform.parent = null; } return vfxInstance; } public GameObject SpawnMuzzleVFX(string effectName, Transform muzzleTransform) { return VFXObject.Spawn(Get(effectName).muzzleVFX, executor, muzzleTransform); } public GameObject SpawnHitVFX(string effectName, Vector3 hitPosition) { return VFXObject.Spawn(Get(effectName).muzzleVFX, executor, hitPosition, Quaternion.identity); } } [Serializable] [HideReferenceObjectPicker] public class VFXUnit { [BoxGroup("VFX"), LabelWidth(150)] [PropertyTooltip("特效预制体")] public GameObject mainVFX; [BoxGroup("VFX"), LabelWidth(150)] [PropertyTooltip("枪口特效(可空)")] public GameObject muzzleVFX; [BoxGroup("VFX"), LabelWidth(150)] [PropertyTooltip("击中特效(可空),通常用于AttackArea获取,不需要使用VFXData生成")] public GameObject hitVFX; [BoxGroup("VFX"), LabelWidth(150)] [PropertyTooltip("附加特效(可空)")] public List otherVFXList; [BoxGroup("Transform"), LabelWidth(150)] [PropertyTooltip("特效生成时的位置偏移")] public Vector3 localPosition = Vector3.zero; [BoxGroup("Transform"), LabelWidth(150)] [PropertyTooltip("特效生成时的旋转角度")] public Vector3 localEulerAngles = Vector3.zero; [BoxGroup("Transform"), LabelWidth(150)] [PropertyTooltip("特效生成时的缩放比例")] public Vector3 localScale = Vector3.one; [BoxGroup("Transform"), LabelWidth(150)] [PropertyTooltip("是否在生成后保持特效与父级Transform的联系")] public bool keepLocalTransform = false; [Title("Tools")] [ShowInInspector] public Vector2 slashScreenDirection => GetSlashScreenDirection(true); [ShowInInspector] public Vector2 slashScreenPosition => GetSlashScreenDirection(false); [OnInspectorInit] public Vector2 GetSlashScreenDirection(bool isRotation) { // 1. 定义屏幕(摄像机)的法线。 // 因为假设了角色和摄像机朝向一致,我们可以直接把这里当做 View Space。 // 摄像机看向 Z+,屏幕平面垂直于 Z 轴,所以法线是 Vector3.forward。 Vector3 screenNormal = Vector3.forward; // 2. 计算刀光平面的法线。 // 刀光原本是 XZ 平面,法线是 Y+ (Vector3.up)。 // 我们通过欧拉角旋转这个法线。 Quaternion rotation = Quaternion.Euler(localEulerAngles); Vector3 slashNormal = rotation * Vector3.up; // 3. 计算交线向量(叉积)。 // 几何原理:两个平面的交线垂直于这两个平面的法线。 Vector3 intersectionLine = Vector3.Cross(slashNormal, screenNormal); // 4. 转换为 2D 向量。 // 因为 intersectionLine 必定在屏幕平面上(垂直于 screenNormal), // 它的 Z 分量理论上应该是 0(或者非常接近 0),我们直接取 XY。 Vector2 screenDir = isRotation ? new Vector2(-intersectionLine.y, intersectionLine.x).normalized : new Vector2(intersectionLine.x, intersectionLine.y).normalized; // 5. 处理特殊情况:平行。 // 如果刀光平面和屏幕平行(比如刀光垂直立起来正对着摄像机), // 叉积结果会是 (0,0,0)。这时候没有“方向”可言。 if (screenDir.sqrMagnitude < 0.0001f) { return Vector2.zero; // 或者根据需求返回默认值 } // 6. 返回结果。 // 将screenDir的x和y都乘以10之后,保留小数点后2位,得到最终的刀光屏幕方向。 return new Vector2( (float)Math.Round(screenDir.x * 10f, 2), (float)Math.Round(screenDir.y * 10f, 2) ); } } }