news 2026/8/4 6:39:41

Unity MyFramework 用法说明(二十四):使用 EffectManager 统一管理特效生命周期与复用

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Unity MyFramework 用法说明(二十四):使用 EffectManager 统一管理特效生命周期与复用

上一篇介绍了PrefabPoolManager,它负责复用 Prefab 实例。但游戏特效除了实例创建,还需要处理粒子播放、拖尾清理、生存时间、时间缩放和所属对象销毁等问题。

MyFramework 使用EffectManagerPrefabPoolManager之上继续封装,统一管理 3D 特效的创建、播放、回收和释放。

项目地址:

https://github.com/ZHOURUIH/MyFramework

一、为什么不能只使用 PrefabPoolManager

通过对象池获得一个特效 Prefab 后,业务仍然需要处理:

找到所有 ParticleSystem 找到所有 TrailRenderer 开始或停止粒子 清除上次残留的拖尾 计算特效生存时间 播放结束后回收 处理暂停和时间缩放 所属角色销毁时回收特效

如果每种技能都重复实现这些逻辑,特效代码很容易散落到角色、技能和场景管理器中。

EffectManager将一个特效封装为:

GameEffect ├── GameObject ├── ParticleSystem列表 ├── TrailRenderer列表 ├── Animator列表 ├── 生存时间 ├── 播放状态 └── 对象池信息

业务层只需要决定:

播放什么特效 播放到什么位置 是否跟随某个对象 持续多长时间

二、创建一个临时特效

先定义特效分类:

public static class EFFECT_TAG { public const int BATTLE = 1; public const int SCENE = 2; public const int UI = 3; }

在指定位置创建一个持续一秒的受击特效:

GameEffect effect = mEffectManager.createEffect( "Effect/Battle/Hit.prefab", null, null, EFFECT_TAG.BATTLE, true, true, hitPosition, 1.0f);

参数分别表示:

资源路径 挂接的框架对象 GameObject父节点 特效Tag 创建后是否激活 是否通过移动到远处隐藏 初始位置 生存时间

资源路径相对于:

Assets/GameResources

并且必须带文件后缀。

如果只是播放一次,不需要保存返回值。生存时间结束后,EffectManager会自动将它回收到特效池。

三、GameEffect 如何管理粒子

GameEffect.setObject()会缓存 Prefab 中的相关组件:

public override void setObject(GameObject obj) { base.setObject(obj); mParticleSystems.Clear(); mTrailRenderers.Clear(); mEffectAnimators.Clear(); if (mTransform == null) { return; } mTransform.GetComponentsInChildren( mParticleSystems); mTransform.GetComponentsInChildren( mTrailRenderers); mTransform.GetComponentsInChildren( mEffectAnimators); foreach (ParticleSystem particle in mParticleSystems) { var main = particle.main; main.cullingMode = ParticleSystemCullingMode.Pause; } mTrailRenderers.For( trail => trail.autodestruct = false); }

这些组件只在构建GameEffect时查找一次,后续复用时直接使用缓存列表。

播放特效:

effect.play();

停止特效:

effect.stop();

停止时会统一处理:

停止全部粒子 清空粒子内容 清除TrailRenderer拖尾 恢复默认时间缩放设置

这样重新播放对象池中的特效时,不会显示上一次残留的拖尾。

四、生存时间如何自动销毁特效

创建特效时可以设置:

effect.setLifeTime(1.0f);

GameEffect.update()中会更新计时器:

if (tickTimerOnce( ref mLifeTimer, elapsedTime)) { mIsDead = true; }

EffectManager.update()会遍历正在使用的特效:

using var a = new SafeListReader<GameEffect>(mEffectList); foreach (GameEffect effect in a.mReadList) { if (effect.isActiveInHierarchy()) { effect.update(elapsedTime); } if (!effect.isDead()) { continue; } if (effect.isInEffectPool()) { destroyEffectInPool(effect); } else { destroyEffectNoPool(effect); } }

特效只需要将自己标记为死亡,最终采用哪种方式回收,由EffectManager统一决定。

五、EffectManager 提供了两层复用

createEffect()并不是直接从PrefabPoolManager获取对象。

它会先尝试从专用的GameEffectPool获取已经构建完成的特效:

GameEffect effect = mGameEffectPool.getOneEffect( parent, nameWithPath, pos, moveToHide, active, lifeTime); if (effect != null) { return mEffectList.add(effect); } return createEffectNoPool( nameWithPath, attachedParent, parent, tag, active, moveToHide, pos, lifeTime, true);

这里存在两层对象池:

PrefabPoolManager 复用GameObject实例 GameEffectPool 复用已经完成组件扫描和初始化的GameEffect

只使用PrefabPoolManager时,重新获取实例后还需要创建GameEffect,并重新收集所有粒子、拖尾和 Animator。

GameEffectPool会保留完整的GameEffect,再次播放时可以直接设置位置和生存时间。

因此,常用临时特效优先使用:

createEffect(...)

只有不希望进入专用特效池时,才直接使用:

createEffectNoPool(...)

这里的NoPool指的是不进入GameEffectPool,底层 GameObject 仍然来自PrefabPoolManager

六、特效池内部如何管理对象

GameEffectPool按照资源路径分别保存使用中和未使用特效:

protected Dictionary< string, SafeList<GameEffect>> mUnusedEffectList = new(); protected Dictionary< string, List<GameEffect>> mInusedEffectList = new();

回收特效时:

public void unuseEffect(GameEffect effect) { mInusedEffectList .get(effect.getFilePath()) ?.Remove(effect); mUnusedEffectList .getOrAddNew(effect.getFilePath()) .add(effect); effect.setUnuseTime(DateTime.Now); }

下次播放同一路径的特效时,优先取出未使用实例:

GameEffect effect = effectList.removeAt( effectList.count() - 1); effect.setLifeTime(lifeTime); effect.setDead(false);

默认超过 60 秒没有再次使用的缓存特效会被真正销毁:

mEffectManager.setUnuseMaxTime(60);

所以特效池并不会永久保留所有曾经播放过的特效。

七、异步创建要关联对象生命周期

角色刚释放技能时开始加载特效,但加载完成前角色可能已经死亡或离开场景。

这种情况应使用安全异步接口:

mEffectManager.createEffectAsyncSafe( "Effect/Battle/Buff.prefab", character, character, true, (effect) => { effect.setPosition( character.getPosition()); }, EFFECT_TAG.BATTLE, true, -1.0f);

这里的两个character分别表示:

relatedObject 用于检查异步加载期间对象是否仍然有效 attachedParent 记录特效挂接在哪个框架对象上

加载完成后,如果relatedObject已经被销毁或从对象池重新分配,特效不会继续交给旧回调。

如果attachedParent被销毁,EffectManager也会自动销毁挂接到它身上的所有特效。

内部会为挂接对象注册销毁回调:

attachedParent.addDestroyCallback( mObjectDestroyCallback); mEffectAttachList .getOrAddListPersist(attachedParent) .Add(effect);

这样角色销毁时,不会遗留仍在播放的 Buff 或技能特效。

八、高频简单粒子使用 QuickEffect

大量角色同时受击时,可能需要在一帧内播放很多相同的火花粒子。

如果每次都获取一个完整 Prefab 实例,即使使用对象池,也要维护大量 GameObject。

这类特效可以使用:

mEffectManager .playEffectAsyncAtPositionQuick( "Effect/Battle/HitSpark.prefab", hitPosition, EFFECT_TAG.BATTLE);

QuickEffect不会为每次播放创建独立节点,而是复用同一个粒子系统,并调用:

item.Emit( emission.GetBurst(j).maxCount);

粒子模拟空间会被设置为世界空间,因此同一套粒子系统可以在不同位置连续发射。

它适合:

非循环粒子 不带拖尾 简单Burst发射 同一特效高频出现在多个位置

不适合:

循环特效 TrailRenderer拖尾 需要独立停止的特效 依赖复杂粒子模块的特效

QuickEffect是针对特定高频场景的优化方式,并不是普通特效的默认替代方案。

九、手动销毁和批量清理

需要提前停止一个特效:

mEffectManager.destroyEffect( ref mBuffEffect);

对于来自GameEffectPool的特效,默认只是回收。

要求真正销毁:

mEffectManager.destroyEffect( ref mBuffEffect, true);

按 Tag 批量销毁:

mEffectManager.destroyAllEffectWithTag( EFFECT_TAG.BATTLE);

适合在退出战斗或切换场景时清理整类特效。

不要直接调用:

Object.Destroy( effect.getGameObject());

否则会绕过EffectManagerGameEffectPoolPrefabPoolManager保存的状态。

十、总结

MyFramework 的特效管理分为三种方式:

createEffect 复用完整GameEffect,适合大多数临时特效 createEffectNoPool 只复用Prefab实例,不缓存完整GameEffect QuickEffect 复用一个粒子系统在多个位置快速发射

完整生命周期是:

加载Prefab ↓ 创建或复用GameEffect ↓ 缓存粒子、拖尾和Animator ↓ 播放并更新生存时间 ↓ 到期或主动销毁 ↓ 回到GameEffectPool ↓ 长时间不用后真正释放

PrefabPoolManager解决的是 Prefab 实例复用,而EffectManager继续解决特效播放状态和业务生命周期。

业务层只需要表达特效的路径、位置、所属对象和持续时间,粒子停止、拖尾清理、对象复用及最终释放都由框架统一处理。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/4 6:38:09

Oracle dblink实战指南:跨库数据桥梁的原理、创建与优化

1. 项目概述&#xff1a;为什么我们需要跨库“搭桥”&#xff1f;在数据库的世界里&#xff0c;数据孤岛是个老生常谈的问题。想象一下&#xff0c;你手头有一个核心的订单数据库&#xff08;我们叫它DB_A&#xff09;&#xff0c;里面存着客户信息和交易记录。同时&#xff0c…

作者头像 李华
网站建设 2026/8/4 6:37:17

竹知了转呀转呀转~ 转不出的公关危机

竹知了事件最讽刺的地方 本来没人当回事&#xff0c;企业一出手&#xff0c;所有人都知道了 一场网络玩梗&#xff0c;为什么会变成企业公关的反面教材极简摘要&#xff5c;竹知了事件真正值得企业研究的&#xff0c;不是玩梗本身&#xff0c;而是企业如何把“法律上可以做的事…

作者头像 李华
网站建设 2026/8/4 6:35:09

C/C++/Java/Python/JavaScript五大编程语言核心对比与选型指南

1. 从“Hello, World!”说起&#xff1a;为什么我们需要这么多语言&#xff1f;如果你刚接触编程&#xff0c;打开教程网站&#xff0c;看到琳琅满目的语言列表——C、C、Java、Python、JavaScript……第一反应很可能是头大。它们不都是用来让电脑干活的吗&#xff1f;为什么不…

作者头像 李华
网站建设 2026/8/4 6:34:39

Spring Boot启动速度优化实战与性能提升

1. 为什么Spring Boot启动速度如此重要&#xff1f;在微服务架构盛行的今天&#xff0c;一个Spring Boot应用的启动时间从3分钟降到30秒&#xff0c;意味着什么&#xff1f;想象一下&#xff1a;当你正在紧急修复线上问题&#xff0c;每次修改后需要等待3分钟才能验证效果&…

作者头像 李华
网站建设 2026/8/4 6:27:27

SMPL-X三维人体建模实战:从原理到单图重建与部署

1. 项目概述&#xff1a;从“火柴人”到“数字真人”的进化 在计算机视觉和图形学领域&#xff0c;让机器理解并重建一个三维的人体&#xff0c;一直是个既迷人又充满挑战的课题。我们常说的“人体捕捉”&#xff0c;其终极目标远不止于得到一个会动的“火柴人”骨架。想象一下…

作者头像 李华
网站建设 2026/8/4 6:26:49

08-向量数据库选型-Chroma-FAISS-Milvus-pgvector

向量数据库怎么选&#xff1f;Chroma、FAISS、Milvus 与 pgvector 对比系列&#xff1a;从零构建企业 RAG 知识库&#xff08;第 8 篇&#xff09;1. 先纠正一个概念 FAISS 更准确地说是向量相似度搜索库&#xff0c;不是完整的网络数据库&#xff1b;Chroma 面向 AI 应用开发体…

作者头像 李华