chore: initial commit
This commit is contained in:
21
Assets/Scripts/VFX/BaseGames.VFX.asmdef
Normal file
21
Assets/Scripts/VFX/BaseGames.VFX.asmdef
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"precompiledReferences": [],
|
||||
"name": "BaseGames.VFX",
|
||||
"defineConstraints": [],
|
||||
"noEngineReferences": false,
|
||||
"versionDefines": [],
|
||||
"rootNamespace": "BaseGames.VFX",
|
||||
"references": [
|
||||
"BaseGames.Core.Events",
|
||||
"BaseGames.Core",
|
||||
"BaseGames.Combat",
|
||||
"BaseGames.Feedback",
|
||||
"Unity.Addressables",
|
||||
"Unity.ResourceManager"
|
||||
],
|
||||
"autoReferenced": true,
|
||||
"overrideReferences": false,
|
||||
"includePlatforms": []
|
||||
}
|
||||
7
Assets/Scripts/VFX/BaseGames.VFX.asmdef.meta
Normal file
7
Assets/Scripts/VFX/BaseGames.VFX.asmdef.meta
Normal file
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c71c756e9181e714aa56edbbf35d4df2
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
42
Assets/Scripts/VFX/HitFXSpawner.cs
Normal file
42
Assets/Scripts/VFX/HitFXSpawner.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
using UnityEngine;
|
||||
using BaseGames.Combat;
|
||||
|
||||
namespace BaseGames.VFX
|
||||
{
|
||||
/// <summary>
|
||||
/// 全局命中特效派发器:订阅 HitConfirmedEventChannel,
|
||||
/// 根据 DamageInfo.FxType 从 VFXCatalog 查找并播放特效。
|
||||
/// 放置在 Persistent 场景的 [Systems] GameObject 上。
|
||||
/// </summary>
|
||||
public class HitFXSpawner : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private HitConfirmedEventChannelSO _onHitConfirmed;
|
||||
[SerializeField] private VFXCatalogSO _catalog;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (_catalog != null)
|
||||
_catalog.Initialize();
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if (_onHitConfirmed != null)
|
||||
_onHitConfirmed.OnEventRaised += HandleHit;
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
if (_onHitConfirmed != null)
|
||||
_onHitConfirmed.OnEventRaised -= HandleHit;
|
||||
}
|
||||
|
||||
private void HandleHit(HitInfo info)
|
||||
{
|
||||
if (_catalog == null || VFXPool.Instance == null) return;
|
||||
|
||||
if (_catalog.TryGetHitFX(info.DamageInfo.FxType, out var vfxRef))
|
||||
VFXPool.Instance.Play(vfxRef, info.HitPoint);
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/VFX/HitFXSpawner.cs.meta
Normal file
11
Assets/Scripts/VFX/HitFXSpawner.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6380807294a34ad4cb0a2b7099712163
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
56
Assets/Scripts/VFX/HurtFlashController.cs
Normal file
56
Assets/Scripts/VFX/HurtFlashController.cs
Normal file
@@ -0,0 +1,56 @@
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
using BaseGames.Feedback;
|
||||
|
||||
namespace BaseGames.VFX
|
||||
{
|
||||
/// <summary>
|
||||
/// 受伤闪白效果控制器。
|
||||
/// 通过 MaterialPropertyBlock 修改 SpriteRenderer 材质的 _FlashAmount 参数(0~1);
|
||||
/// 对应 Shader 需暴露 _FlashColor 与 _FlashAmount 两个属性。
|
||||
/// 调用 Flash() 触发一次闪白动画(Coroutine 实现,不依赖 UniTask)。
|
||||
/// </summary>
|
||||
public class HurtFlashController : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private SpriteRenderer _renderer;
|
||||
[SerializeField] private FeedbackConfigSO _config;
|
||||
|
||||
private static readonly int FlashColorID = Shader.PropertyToID("_FlashColor");
|
||||
private static readonly int FlashAmountID = Shader.PropertyToID("_FlashAmount");
|
||||
|
||||
private MaterialPropertyBlock _block;
|
||||
private Coroutine _flashCoroutine;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (_renderer == null)
|
||||
_renderer = GetComponent<SpriteRenderer>();
|
||||
_block = new MaterialPropertyBlock();
|
||||
}
|
||||
|
||||
/// <summary>触发一次闪白动画。若上一次闪白未结束则重置计时器。</summary>
|
||||
public void Flash()
|
||||
{
|
||||
if (_flashCoroutine != null)
|
||||
StopCoroutine(_flashCoroutine);
|
||||
_flashCoroutine = StartCoroutine(FlashCoroutine());
|
||||
}
|
||||
|
||||
private IEnumerator FlashCoroutine()
|
||||
{
|
||||
SetFlash(1f);
|
||||
yield return new WaitForSeconds(_config != null ? _config.HurtFlashDuration : 0.12f);
|
||||
SetFlash(0f);
|
||||
_flashCoroutine = null;
|
||||
}
|
||||
|
||||
private void SetFlash(float amount)
|
||||
{
|
||||
_renderer.GetPropertyBlock(_block);
|
||||
if (_config != null)
|
||||
_block.SetColor(FlashColorID, _config.HurtFlashColor);
|
||||
_block.SetFloat(FlashAmountID, amount);
|
||||
_renderer.SetPropertyBlock(_block);
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/VFX/HurtFlashController.cs.meta
Normal file
11
Assets/Scripts/VFX/HurtFlashController.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0913d359cdb3e3849b286bc6e7cc311f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
66
Assets/Scripts/VFX/VFXCatalogSO.cs
Normal file
66
Assets/Scripts/VFX/VFXCatalogSO.cs
Normal file
@@ -0,0 +1,66 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.AddressableAssets;
|
||||
using BaseGames.Combat;
|
||||
|
||||
namespace BaseGames.VFX
|
||||
{
|
||||
/// <summary>
|
||||
/// VFX 资产映射字典:HitFxType → VFX Prefab Addressable 引用。
|
||||
/// 在 GameManager.OnGameplayStarted 中调用 Initialize() 建立快速查表。
|
||||
/// 资产路径:Assets/ScriptableObjects/VFX/VFX_Catalog.asset
|
||||
/// </summary>
|
||||
[CreateAssetMenu(menuName = "VFX/VFXCatalog")]
|
||||
public class VFXCatalogSO : ScriptableObject
|
||||
{
|
||||
[Header("命中特效映射")]
|
||||
public VFXEntry[] hitEffects;
|
||||
|
||||
[Header("预热配置")]
|
||||
public VFXWarmupEntry[] warmups;
|
||||
|
||||
private Dictionary<HitFxType, AssetReferenceGameObject> _map;
|
||||
|
||||
/// <summary>建立快速查表字典。在 Gameplay 开始前调用一次。</summary>
|
||||
public void Initialize()
|
||||
{
|
||||
_map = new Dictionary<HitFxType, AssetReferenceGameObject>();
|
||||
if (hitEffects == null) return;
|
||||
foreach (var e in hitEffects)
|
||||
_map[e.type] = e.vfxRef;
|
||||
}
|
||||
|
||||
/// <summary>根据 HitFxType 查找对应 VFX Prefab 引用。</summary>
|
||||
public bool TryGetHitFX(HitFxType type, out AssetReferenceGameObject vfxRef)
|
||||
{
|
||||
if (_map != null)
|
||||
return _map.TryGetValue(type, out vfxRef);
|
||||
|
||||
// 未初始化时回退至线性查找
|
||||
if (hitEffects != null)
|
||||
{
|
||||
foreach (var e in hitEffects)
|
||||
{
|
||||
if (e.type == type) { vfxRef = e.vfxRef; return true; }
|
||||
}
|
||||
}
|
||||
vfxRef = default;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public struct VFXEntry
|
||||
{
|
||||
public HitFxType type;
|
||||
public AssetReferenceGameObject vfxRef;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public struct VFXWarmupEntry
|
||||
{
|
||||
public AssetReferenceGameObject vfxRef;
|
||||
[Min(1)] public int warmupCount;
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/VFX/VFXCatalogSO.cs.meta
Normal file
11
Assets/Scripts/VFX/VFXCatalogSO.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a55d31d8ca14fa5498d42bc28a4189f5
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
131
Assets/Scripts/VFX/VFXPool.cs
Normal file
131
Assets/Scripts/VFX/VFXPool.cs
Normal file
@@ -0,0 +1,131 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.AddressableAssets;
|
||||
|
||||
namespace BaseGames.VFX
|
||||
{
|
||||
/// <summary>
|
||||
/// ParticleSystem 专用对象池。挂在 Persistent 场景 [VFXPool] GameObject 上。
|
||||
/// 粒子播放完成(或超过 MaxLifetime)后自动回池,调用方无需手动归还。
|
||||
/// 不依赖 UniTask,使用 Coroutine 驱动回收。
|
||||
/// </summary>
|
||||
public class VFXPool : MonoBehaviour
|
||||
{
|
||||
public static VFXPool Instance { get; private set; }
|
||||
|
||||
/// <summary>全局兜底超时(秒)。防止循环粒子永不回池。</summary>
|
||||
[SerializeField, Min(1f)] private float _globalMaxLifetime = 10f;
|
||||
|
||||
private readonly Dictionary<AssetReferenceGameObject, Queue<ParticleSystem>> _pools
|
||||
= new Dictionary<AssetReferenceGameObject, Queue<ParticleSystem>>();
|
||||
|
||||
private void Awake() => Instance = this;
|
||||
|
||||
/// <summary>
|
||||
/// 在世界坐标播放一次特效(Fire-and-forget,Coroutine 自动回池)。
|
||||
/// </summary>
|
||||
/// <param name="maxLifetime">>0 时覆盖全局超时;≤0 时使用 _globalMaxLifetime。</param>
|
||||
public void Play(AssetReferenceGameObject vfxRef,
|
||||
Vector3 position,
|
||||
Quaternion rotation = default,
|
||||
float maxLifetime = 0f)
|
||||
{
|
||||
StartCoroutine(PlayCoroutine(vfxRef, position, rotation, maxLifetime));
|
||||
}
|
||||
|
||||
/// <summary>预热:预先创建若干实例避免首次播放卡顿。</summary>
|
||||
public void Warmup(AssetReferenceGameObject vfxRef, int count)
|
||||
{
|
||||
StartCoroutine(WarmupCoroutine(vfxRef, count));
|
||||
}
|
||||
|
||||
// ── 内部实现 ─────────────────────────────────────────────────────────────
|
||||
private IEnumerator PlayCoroutine(AssetReferenceGameObject vfxRef,
|
||||
Vector3 position,
|
||||
Quaternion rotation,
|
||||
float maxLifetime)
|
||||
{
|
||||
ParticleSystem ps = null;
|
||||
|
||||
// 从池中取或异步创建
|
||||
if (TryDequeue(vfxRef, out ps))
|
||||
{
|
||||
// 直接使用
|
||||
}
|
||||
else
|
||||
{
|
||||
// 异步加载并实例化
|
||||
var op = Addressables.InstantiateAsync(vfxRef, transform);
|
||||
yield return op;
|
||||
if (op.Result == null)
|
||||
{
|
||||
Debug.LogError($"[VFXPool] Failed to instantiate VFX: {vfxRef.RuntimeKey}");
|
||||
yield break;
|
||||
}
|
||||
ps = op.Result.GetComponent<ParticleSystem>();
|
||||
if (ps == null)
|
||||
{
|
||||
Debug.LogError($"[VFXPool] No ParticleSystem on VFX prefab: {vfxRef.RuntimeKey}");
|
||||
Addressables.ReleaseInstance(op.Result);
|
||||
yield break;
|
||||
}
|
||||
ps.gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
// 播放
|
||||
ps.transform.SetPositionAndRotation(position, rotation);
|
||||
ps.gameObject.SetActive(true);
|
||||
ps.Play();
|
||||
|
||||
float limit = maxLifetime > 0f ? maxLifetime : _globalMaxLifetime;
|
||||
float elapsed = 0f;
|
||||
|
||||
// 等待粒子结束或超时
|
||||
while (elapsed < limit && ps.IsAlive(true))
|
||||
{
|
||||
elapsed += Time.deltaTime;
|
||||
yield return null;
|
||||
}
|
||||
|
||||
if (ps.IsAlive(true))
|
||||
{
|
||||
ps.Stop(true, ParticleSystemStopBehavior.StopEmittingAndClear);
|
||||
Debug.LogWarning(
|
||||
$"[VFXPool] '{vfxRef.RuntimeKey}' 超过 {limit:F1}s 强制回收。" +
|
||||
" 请检查粒子是否设为 Loop 或 Duration 过长。");
|
||||
}
|
||||
|
||||
ps.gameObject.SetActive(false);
|
||||
Enqueue(vfxRef, ps);
|
||||
}
|
||||
|
||||
private IEnumerator WarmupCoroutine(AssetReferenceGameObject vfxRef, int count)
|
||||
{
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var op = Addressables.InstantiateAsync(vfxRef, transform);
|
||||
yield return op;
|
||||
if (op.Result == null) continue;
|
||||
var ps = op.Result.GetComponent<ParticleSystem>();
|
||||
if (ps == null) { Addressables.ReleaseInstance(op.Result); continue; }
|
||||
ps.gameObject.SetActive(false);
|
||||
Enqueue(vfxRef, ps);
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryDequeue(AssetReferenceGameObject key, out ParticleSystem ps)
|
||||
{
|
||||
ps = null;
|
||||
return _pools.TryGetValue(key, out var q) && q.Count > 0 && (ps = q.Dequeue()) != null;
|
||||
}
|
||||
|
||||
private void Enqueue(AssetReferenceGameObject key, ParticleSystem ps)
|
||||
{
|
||||
if (!_pools.ContainsKey(key))
|
||||
_pools[key] = new Queue<ParticleSystem>();
|
||||
_pools[key].Enqueue(ps);
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/VFX/VFXPool.cs.meta
Normal file
11
Assets/Scripts/VFX/VFXPool.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1f2e57f42abd15c419f9013416d4a6d8
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user