2025-11-25 08:19:33 -05:00
|
|
|
|
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<string, VFXUnit> collection = new OrderedDictionary<string, VFXUnit>();
|
2026-06-13 18:43:40 -04:00
|
|
|
|
|
|
|
|
|
|
public VFXUnit Get(string effectName)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (string.IsNullOrEmpty(effectName)) return null;
|
|
|
|
|
|
return collection.GetValueOrDefault(effectName);
|
|
|
|
|
|
}
|
2025-11-25 08:19:33 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public partial class VFXData
|
|
|
|
|
|
{
|
2026-05-23 08:27:50 -04:00
|
|
|
|
public GameObject SpawnVFX(string vfxName, CharacterBase executor, Vector3 position, Quaternion rotation)
|
2025-11-25 08:19:33 -05:00
|
|
|
|
{
|
2026-05-23 08:27:50 -04:00
|
|
|
|
VFXUnit vfxUnit = Get(vfxName);
|
|
|
|
|
|
GameObject vfxInstance = VFXObject.Spawn(vfxUnit.mainVFX, executor, position, rotation);
|
|
|
|
|
|
vfxInstance.transform.localScale = vfxUnit.localScale;
|
|
|
|
|
|
return vfxInstance;
|
2025-11-25 08:19:33 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-23 08:27:50 -04:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 生成特效。executor 和 startTransform 必须显式传入,避免共享 ScriptableObject 缓存错误引用。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public GameObject SpawnVFX(string vfxName, CharacterBase executor, Transform startTransform = null)
|
2025-11-25 08:19:33 -05:00
|
|
|
|
{
|
2026-05-23 08:27:50 -04:00
|
|
|
|
startTransform ??= executor.transform;
|
2025-11-25 08:19:33 -05:00
|
|
|
|
VFXUnit vfxUnit = Get(vfxName);
|
|
|
|
|
|
|
2025-12-17 04:19:38 -05:00
|
|
|
|
GameObject vfxInstance = VFXObject.Spawn(vfxUnit.mainVFX, executor, startTransform);
|
2025-11-25 08:19:33 -05:00
|
|
|
|
Transform vfxTransform = vfxInstance.transform;
|
|
|
|
|
|
|
|
|
|
|
|
vfxTransform.localPosition = vfxUnit.localPosition;
|
|
|
|
|
|
vfxTransform.localEulerAngles = vfxUnit.localEulerAngles;
|
|
|
|
|
|
vfxTransform.localScale = vfxUnit.localScale;
|
|
|
|
|
|
|
|
|
|
|
|
if (!vfxUnit.keepLocalTransform)
|
|
|
|
|
|
{
|
|
|
|
|
|
vfxTransform.parent = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return vfxInstance;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-23 08:27:50 -04:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 生成枪口特效。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public GameObject SpawnMuzzleVFX(string effectName, CharacterBase executor, Transform muzzleTransform)
|
2025-11-25 08:19:33 -05:00
|
|
|
|
{
|
2026-05-23 08:27:50 -04:00
|
|
|
|
return VFXObject.Spawn(Get(effectName).muzzleVFX, executor, muzzleTransform);
|
2025-11-25 08:19:33 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-23 08:27:50 -04:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 生成击中特效。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public GameObject SpawnHitVFX(string effectName, CharacterBase executor, Vector3 hitPosition)
|
2025-11-25 08:19:33 -05:00
|
|
|
|
{
|
2026-05-23 08:27:50 -04:00
|
|
|
|
return VFXObject.Spawn(Get(effectName).muzzleVFX, executor, hitPosition, Quaternion.identity);
|
2025-11-25 08:19:33 -05:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[Serializable]
|
2026-01-17 11:35:49 -05:00
|
|
|
|
[HideReferenceObjectPicker]
|
2025-11-25 08:19:33 -05:00
|
|
|
|
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<GameObject> 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;
|
2026-03-20 12:07:44 -04:00
|
|
|
|
|
|
|
|
|
|
[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)
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
2025-11-25 08:19:33 -05:00
|
|
|
|
}
|
|
|
|
|
|
}
|