using System; using System.Collections.Generic; using Games.AnimationModule; using Games.Events; using Games.GlobeDefine; using Games.LogicObj; using Games.Scene; using GCGame.Table; using Module.Log; using UnityEngine; using UnityEngine.Events; using Object = UnityEngine.Object; /// /// /// 最基本的特效管理器 /// public abstract class BaseEffectLogic : MonoBehaviour { /// /// 最小加载容忍时间,加载时间超过该值会忽略特效 /// public const float minLoadTolerance = 0.5f; /// /// 最小加载容忍时间比例,加载时间超过特效总时长比例会忽略特效 /// public const float minLoadRatio = 0.3f; private bool _enable; public List ActiveList { get; private set; } public List DelayList { get; private set; } public bool IsInited { get; private set; } /// /// 获得特效后,如果是需要延迟出现的情况,特效将会延迟出现 /// /// 特效物体 /// 加载数据 public void OnCreateEffect(Transform effect, EffectLoadData loadData) { // 加载超过容忍时间后,直接忽略当前特效 var loadDelay = Time.time - loadData.startTime; var effectDelay = Mathf.Max(0f, loadData.data.DelayTime + loadData.delayModifier); var tolerance = effectDelay + Mathf.Max(minLoadTolerance, minLoadRatio * loadData.data.Duration); if (loadDelay > tolerance) { IndependentEffectManager.PushEffect(loadData.data, effect); } else { // 从加载开始所计算的延迟时间 effectDelay = Mathf.Max(0f, effectDelay - loadDelay); if (effectDelay > 0) { effect.gameObject.SetActive(false); DelayList.Add(new DelayEffectReference(loadData, effect, Time.time + effectDelay)); } else { ActiveEffect(effect, loadData); } } } public virtual Transform GetBindPoint(string pointName, out Vector3 offset) { // 故意保留的强制报错,实际只有角色的需要拥有获取绑定点的功能,其他的只保留一个编译接口 throw new InvalidOperationException(string.Format("{0}未实现获取绑定点的功能,不应该使用绑定类型特效!", GetType())); } public virtual Obj_Character GetOwnerCharacter() { // 故意保留的强制报错,实际只有角色才能获得拥有者,其他的只保留一个编译接口 throw new InvalidOperationException(string.Format("{0}未实现获取拥有者角色的功能,不应该使用神像类型特效!", GetType())); } private void ActiveEffect(Transform effect, EffectLoadData loadData) { CommonUtility.SetLayersRecursively(effect, loadData.layer); var activeResult = ActiveEffectInherited(effect, loadData); if (loadData.playCallback != null) loadData.playCallback(activeResult ? effect.gameObject : null, loadData.playParameter); } protected bool ActiveEffectInherited(Transform effect, EffectLoadData loadData) { var result = false; var customData = (CustomEffectLoadData) loadData.customData; switch (customData.EffectType) { case CommonEffectType.Bound: { var duration = loadData.data.Duration; if (duration < 0) duration = float.PositiveInfinity; var activeEffectData = new CommonEffectReference(effect, loadData.data, loadData.handle, Time.time + duration); Vector3 offset; var bindPoint = GetBindPoint(loadData.data.ParentName, out offset); activeEffectData.SetBindPoint(bindPoint, offset + new Vector3(loadData.data.OffsetX, loadData.data.OffsetY, loadData.data.OffsetZ)); ActiveList.Add(activeEffectData); // Hack:给预警圈的特别处理 SetWarningData(effect, loadData.data); result = true; } break; case CommonEffectType.Position: { var effectData = (NormalEffectCustomData) customData; var duration = effectData.duration > 0f ? effectData.duration : loadData.data.Duration; if (duration < 0) duration = float.PositiveInfinity; var activeEffectData = new CommonEffectReference(effect, loadData.data, loadData.handle, Time.time + duration); effect.position = effectData.position; // NoRotation不需要旋转 var rotation = loadData.data.RotationType == EffectRotationType.noRotation ? Quaternion.identity : effectData.rotation; if (loadData.data.Yaw > 0) rotation = rotation * Quaternion.Euler(0f, loadData.data.Yaw, 0f); effect.rotation = rotation; effect.localScale = loadData.data.Scale > 0 ? Vector3.one * loadData.data.Scale : Vector3.one; ActiveList.Add(activeEffectData); // Hack:给预警圈的特别处理 SetWarningData(effect, loadData.data); result = true; } break; case CommonEffectType.Bullet: { var effectData = (BulletCustomData) customData; var source = Singleton.Instance.FindObjCharacterInScene(effectData.sourceId); if (source != null) { var activeBulletData = new BulletEffectReference(effect, effectData.targetId, source, effectData.bulletData, loadData.data, loadData.handle); effect.position = source.transform.position + source.transform.rotation * activeBulletData.GetStartOffset(); var delay = Mathf.Max(0f, Time.time - loadData.startTime - loadData.data.DelayTime); // 模拟子弹加载期间的位移 if (activeBulletData.InitBullet(delay)) { ActiveList.Add(activeBulletData); result = true; } } if (!result) IndependentEffectManager.PushEffect(loadData.data, effect); } break; case CommonEffectType.BulletToPos: { var effectData = (BulletToPosData) customData; var activeBulletData = new BulletToPosReference(effect, effectData.startPos, effectData.endPos, effectData.bulletData, loadData.data, loadData.handle); ActiveList.Add(activeBulletData); result = true; } break; case CommonEffectType.Chain: { var effectData = (ChainCustomData) customData; var activeChainData = new ChainEffectReference(effect, effectData, loadData.data, loadData.handle); ActiveList.Add(activeChainData); result = true; } break; case CommonEffectType.Stamp: { var effectData = (StampCustomData) customData; var activeEffectData = new StampEffectReference(effect, loadData.data, effectData.stampCount, loadData.handle); Vector3 offset; var bindPoint = GetBindPoint(loadData.data.ParentName, out offset); activeEffectData.SetBindPoint(bindPoint, offset + new Vector3(loadData.data.OffsetX, loadData.data.OffsetY, loadData.data.OffsetZ)); ActiveList.Add(activeEffectData); result = true; } break; case CommonEffectType.WuChangLink: { var effectData = (WuChangLinkData) customData; var activeLinkRef = new WuChangLinkReference(effect, effectData, loadData.data, loadData.handle); ActiveList.Add(activeLinkRef); result = true; } break; case CommonEffectType.WuChangAirport: { var effectData = (WuChangAirportData) customData; var airportReference = new WuChangAirportReference(effect, effectData, loadData.data, loadData.handle); ActiveList.Add(airportReference); result = true; } break; case CommonEffectType.Meteor: { var effectData = (MeteorEffectData) customData; var meteorRef = new MeteorEffectReference(effect, effectData, loadData.data, loadData.handle); ActiveList.Add(meteorRef); result = true; } break; case CommonEffectType.Avatar: { var character = GetOwnerCharacter(); if (character != null) { var duration = loadData.data.Duration; if (duration < 0) duration = float.PositiveInfinity; var activeEffectData = new AvatarEffectReference(effect, loadData.data, loadData.handle, Time.time + duration); activeEffectData.Init(character); ActiveList.Add(activeEffectData); result = true; } } break; default: LogModule.ErrorLog(string.Format("未处理类型为{0}的特效!", customData.EffectType)); break; } return result; } protected void Init() { IsInited = true; ActiveList = new List(); DelayList = new List(); TryAddEvent(); } protected virtual void OnEnable() { _enable = true; TryAddEvent(); } protected virtual void OnDisable() { _enable = false; TryAddEvent(); } private void SetWarningData(Transform effect, Tab_Effect data) { // 特殊处理技能警告指示器 - 暂时简单做法是用普通加载流程,然后只在配置位置时配置效果 var effectType = (EffectLogic.EffectType) data.Type; switch (effectType) { case EffectLogic.EffectType.TYPE_CIRCLE: effect.GetComponent().SetEffectData(data); break; case EffectLogic.EffectType.TYPE_RECTANGLE: effect.GetComponent().SetEffectData(data); break; case EffectLogic.EffectType.TYPE_SECTOR: effect.GetComponent().SetEffectData(data); break; } } private void OnDestroy() { if (GameManager.gameManager != null && GameManager.gameManager.effectPool != null) GameManager.gameManager.effectPool.DestroyEffectLogic(this); } private void TryAddEvent() { if (IsInited && _enable) EventDispatcher.Instance.Add(Games.Events.EventId.PostMainCameraMove, AfterCameraMovement); else EventDispatcher.Instance.Remove(Games.Events.EventId.PostMainCameraMove, AfterCameraMovement); } protected virtual void AfterCameraMovement(object args) { for (var i = DelayList.Count - 1; i >= 0; i--) try { if (DelayList[i].effect == null) { #if UNITY_EDITOR LogModule.ErrorLog(string.Format("一个于{1}的特效{0}被使用不正常的方式删除!", DelayList[i].loadData.data.EffectID, transform.GetHierarchyName())); #endif DelayList.RemoveAt(i); } // 处于延迟发布的状态 else if (DelayList[i].CheckActive()) { var delayReference = DelayList[i]; DelayList.RemoveAt(i); // 可能某个主角模型在Delay阶段被析构,导致绑定的特效被析构 if (delayReference.effect != null) { delayReference.effect.gameObject.SetActive(true); ActiveEffect(delayReference.effect, delayReference.loadData); } } } catch (Exception e) { LogModule.ErrorLog(e.ToString()); } for (var i = ActiveList.Count - 1; i >= 0; i--) try { // 处于延迟回收状态 if (ActiveList[i].effect == null) { #if UNITY_EDITOR LogModule.ErrorLog(string.Format("一个于{1}的特效{0}被使用不正常的方式删除!", ActiveList[i].data.EffectID, transform.GetHierarchyName())); #endif ActiveList.RemoveAt(i); } else if (ActiveList[i].DelayRecovery) { if (!ActiveList[i].UpdateForDelayRecovery()) RemoveEffectAt(i); } else { if (!ActiveList[i].Update()) if (!ActiveList[i].CheckDelayRecovery()) RemoveEffectAt(i); } } catch (Exception e) { LogModule.ErrorLog(e.ToString()); } } public void RemoveEffectByHandle(int handleId) { if (handleId != 0) { IndependentEffectManager.CancelLoadByHandleId(handleId); for (var i = ActiveList.Count - 1; i >= 0; i--) try { if (ActiveList[i].effectHandle == handleId) RemoveEffectAt(i); } catch (Exception e) { LogModule.ErrorLog(e.ToString()); } for (var i = DelayList.Count - 1; i >= 0; i--) try { if (DelayList[i].loadData.handle == handleId) RemoveDelayAt(i); } catch (Exception e) { LogModule.ErrorLog(e.ToString()); } } } public void RemoveEffectByEffectId(int effectId) { IndependentEffectManager.CancelLoad(a => a.data.EffectID == effectId); for (var i = ActiveList.Count - 1; i >= 0; i--) try { if (ActiveList[i].data.EffectID == effectId) RemoveEffectAt(i); } catch (Exception e) { LogModule.ErrorLog(e.ToString()); } for (var i = DelayList.Count - 1; i >= 0; i--) try { if (DelayList[i].loadData.data.EffectID == effectId) RemoveDelayAt(i); } catch (Exception e) { LogModule.ErrorLog(e.ToString()); } } /// /// 清除全部播放中的特效 /// public void CleanEffect() { // 清除加载中的特效 IndependentEffectManager.CleanEffectLogic(this); } protected void RemoveEffectAt(int index) { var item = ActiveList[index]; ActiveList.RemoveAt(index); IndependentEffectManager.PushEffect(item); } protected void RemoveDelayAt(int index) { var delayReference = DelayList[index]; DelayList.RemoveAt(index); IndependentEffectManager.PushEffect(delayReference.loadData.data, delayReference.effect); } /// /// 从延迟列表获得HandleId符合条件的物体 /// // 特殊处理HandleId为0的情况 - 0视为不可用id public DelayEffectReference GetDelayByHandle(int? handleId) { if (handleId == null) return null; return DelayList.Find(a => a.loadData.handle == handleId); } /// /// 从激活列表获得HandleId符合条件的物体 /// // 延迟回收的特效不允许获得 public BaseEffectReference GetReferenceByHandle(int? handleId) { return handleId == null ? null : ActiveList.Find(a => a.effectHandle == handleId && !a.DelayRecovery); } /// /// 从激活列表获得EffectId符合条件的物体 /// // 延迟回收的特效不允许获得 public BaseEffectReference GetReferenceByEffectId(int effectId) { var result = ActiveList.Find(a => a.data.EffectID == effectId && !a.DelayRecovery); return result; } public static bool ValidEffectByCount(Tab_Effect data, GameDefine_Globe.OBJ_TYPE objType) { var result = true; if (objType == GameDefine_Globe.OBJ_TYPE.OBJ_OTHER_PLAYER || //其他玩家 objType == GameDefine_Globe.OBJ_TYPE.OBJ_NPC || //NPC objType == GameDefine_Globe.OBJ_TYPE.OBJ_FELLOW || //伙伴 objType == GameDefine_Globe.OBJ_TYPE.OBJ_ZOMBIE_PLAYER || //僵尸玩家 objType == GameDefine_Globe.OBJ_TYPE.OBJ_DROP_ITEM)//掉落包 { result = false; if (data.CountLimit >= EffectLimitLevel.unlimit) result = true; // 原始逻辑,一般认为unlimit的是非技能特效 else if (PlayerPreferenceData.SystemSkillEffectEnable) { var limit = data.CountLimit < EffectLimitLevel.important ? PlayerPreferenceData.effectCountLimit.commonCount : PlayerPreferenceData.effectCountLimit.importantCount; if (limit > GameManager.gameManager.effectPool.inUseCount) result = true; } } return result; } } public class DelayEffectReference { public readonly Transform effect; public readonly EffectLoadData loadData; public readonly float startTime; public DelayEffectReference(EffectLoadData loadData, Transform effect, float startTime) { this.loadData = loadData; this.effect = effect; this.startTime = startTime; } public bool CheckActive() { return Time.time > startTime; } } public abstract class BaseEffectReference { public readonly Tab_Effect data; public readonly Transform effect; public readonly int? effectHandle; protected BaseEffectReference(Tab_Effect data, Transform effect, int? effectHandle) { this.data = data; this.effect = effect; this.effectHandle = effectHandle; } // 特效是否需要延迟回收 - 有拖尾等特效效果等待效果结束 public bool DelayRecovery { get; private set; } // 特效延迟回收时间 public float DelayRecoveryTime { get; private set; } /// /// 更新特效状态 /// /// 特效是否继续存在 public virtual bool Update() { return effect != null; } /// /// 检查特效是否需要延迟回收 /// public bool CheckDelayRecovery() { var cleaner = effect.GetComponent(); if (cleaner == null) { LogModule.ErrorLog(string.Format("特效{0}上没有ParticleCleaner!", effect.GetHierarchyName())); } else if (cleaner.DelayRecoveryTime > 0f) { DelayRecovery = true; DelayRecoveryTime = Time.time + cleaner.DelayRecoveryTime; cleaner.StartDelayRecovery(); } return DelayRecovery; } /// /// 检查特效是否延迟回收尚未结束 /// public virtual bool UpdateForDelayRecovery() { return Time.time < DelayRecoveryTime; } /// /// 特效播放结束时统一处理 /// public virtual void EndEffect() { } } public delegate void PlayEffectDelegate(GameObject effectObj, object param); #region 特效物品通用管理池 public class EffectPool : AutoLoadPool { public EffectPool(Transform root, UnityAction createAction) : base(root, true, createAction) { } public static string GetBundleName(string assetPath) { return LoadAssetBundle.FixBundleName(LoadAssetBundle.BUNDLE_PATH_EFFECT + assetPath); } public void PreloadEffect(Tab_Effect data) { var bundleName = GetBundleName(data.Path); Preload(bundleName, data.Path); } public void PullEffect(EffectLoadData taskStarter) { var bundleName = GetBundleName(taskStarter.data.Path); PullItem(taskStarter, bundleName, taskStarter.data.Path); } public void PushEffect(BaseEffectReference effect) { PushEffect(effect.data, effect.effect); } public void PushEffect(Tab_Effect data, Transform effect) { var bundleName = GetBundleName(data.Path); if (data.IsOnlyDeactive) { PushItem(bundleName, data.Path, effect); } else { // 不退回物体,但是要求减少使用记录 ReduceCount(bundleName, data.Path); Object.Destroy(effect.gameObject); } } /// /// 按照特效句柄取消一个特效加载任务 /// /// 特效句柄 // 注:主要用于特效在完成加载前,服务器要求停止播放的情况 public void CancelLoadByHandleId(int handleId) { //if (!Killed) for (var i = loadTaskList.Count - 1; i >= 0; i--) loadTaskList[i].CancelByHandle(handleId); } } /// /// 全局唯一的特效管理池,统一处理所有特效GameObject的回收和再利用 /// public class CentralEffectPool : EffectPool { public CentralEffectPool(Transform root) : base(root, EnsureParticleCleaner) { } private static void EnsureParticleCleaner(Transform child) { // 如果没有自定义粒子回收器,就添加通用回收器 var cleaner = child.GetComponent(); if (cleaner == null) child.gameObject.AddComponent(); } /// /// EffectLogic被销毁时,通知池子取消加载注册 /// public void CleanEffectLogic(BaseEffectLogic effectLogic) { // if (!Killed) // { // 移除对加载事件的监听 for (var i = 0; i < loadTaskList.Count; i++) try { loadTaskList[i].RemoveListener(a => a.callback == effectLogic.OnCreateEffect); } catch (Exception e) { LogModule.ErrorLog(e.ToString()); } // 回收使用中的特效 for (var i = 0; i < effectLogic.ActiveList.Count; i++) if (effectLogic.ActiveList[i].effect != null) PushEffect(effectLogic.ActiveList[i]); for (var i = 0; i < effectLogic.DelayList.Count; i++) if (effectLogic.DelayList[i].effect != null) { var delayItem = effectLogic.DelayList[i]; PushEffect(delayItem.loadData.data, delayItem.effect); } effectLogic.ActiveList.Clear(); effectLogic.DelayList.Clear(); } public void DestroyEffectLogic(BaseEffectLogic effectLogic) { if (!GameManager.applicationQuit) { // 移除对加载事件的监听 for (var i = 0; i < loadTaskList.Count; i++) try { loadTaskList[i].RemoveListener(a => a.callback == effectLogic.OnCreateEffect); } catch (Exception e) { LogModule.ErrorLog(e.ToString()); } // 回收会导致报错,因此跳过回收流程 for (var i = 0; i < effectLogic.ActiveList.Count; i++) if (effectLogic.ActiveList[i].effect != null) { var assetName = effectLogic.ActiveList[i].data.Path; var bundleName = GetBundleName(assetName); ReduceCount(bundleName, assetName); } for (var i = 0; i < effectLogic.DelayList.Count; i++) if (effectLogic.DelayList[i].effect != null) { var assetName = effectLogic.DelayList[i].loadData.data.Path; var bundleName = GetBundleName(assetName); ReduceCount(bundleName, assetName); } } } // 主角技能特效预加载 // 预加载方式为在这个地方添加一次引用防止成为Unused #region MainCharacter Preload Effect // 注:其他角色的技能表信息是无法获得的,因此只能预加载主角特效 private readonly List _holdEffects = new List(); public void SetMainPlayerPreload() { var preloadList = new HashSet(); var playerDataPool = GameManager.gameManager.PlayerDataPool; foreach (var ownSkill in playerDataPool.OwnSkillInfo) { if (ownSkill.SkillExTable != null) { var preload = ownSkill.SkillExTable.PreloadEffect; if (!string.IsNullOrEmpty(preload) && !"-1".Equals(preload) && !"0".Equals(preload)) { var preloads = preload.Split(';'); for (var i = 0; i < preloads.Length; i++) { int effectId; if (int.TryParse(preloads[i], out effectId)) { if (effectId > GlobeVar.INVALID_ID) { var effectData = TableManager.GetEffectByID(effectId, 0); if (effectData != null) preloadList.Add(effectData.Path); } } } } } } SetHoldPreload(preloadList); } public void SetHoldPreload(ICollection effects) { for (var i = _holdEffects.Count - 1; i >= 0; i--) { if (!effects.Contains(_holdEffects[i])) { var assetName = _holdEffects[i]; var bundleName = GetBundleName(assetName); ReduceCount(bundleName, assetName, true); _holdEffects.RemoveAt(i); } } foreach (var effect in effects) if (!_holdEffects.Contains(effect)) { _holdEffects.Add(effect); var assetName = effect; var bundleName = GetBundleName(assetName); // 如果已经有池,就直接添加数量 if (!Preload(bundleName, assetName)) AddCount(bundleName, assetName, true); } } // 只有Kill会清空Pool;Kill后也不会重用池,因此不需要额外调用这个。 public void CleanHoldPreload() { for (var i = 0; i < _holdEffects.Count; i++) { var assetName = _holdEffects[i]; var bundleName = GetBundleName(assetName); ReduceCount(bundleName, assetName, true); } _holdEffects.Clear(); } // 底层锁死Pool只在资源加载完成才构造,因此只能监听加载完成后,再执行Hold; // 修改底层风险过高,因此采用加载完成加Token的流程 protected override void OnLoadTaskFinished(EffectLoadTask loadTask) { base.OnLoadTaskFinished(loadTask); if (_holdEffects.Contains(loadTask.PrefabName)) AddCount(loadTask.BundleName, loadTask.PrefabName, true); } #endregion } public class EffectLoadTask : BaseLoadTask { /// /// 按照特效句柄Id取消一个加载需求 /// /// 特效句柄Id /// 是否加载任务已空 public void CancelByHandle(int handle) { for (var i = taskListeners.Count - 1; i >= 0; i--) if (taskListeners[i].handle == handle) taskListeners.RemoveAt(i); } } public class EffectLoadData : BaseLoadData { public readonly Tab_Effect data; /// /// 特效播放延迟的修正数值 /// public readonly float delayModifier; public readonly int? handle; public readonly int layer; public readonly PlayEffectDelegate playCallback; public readonly object playParameter; public readonly float startTime; public object customData; public EffectLoadData(BaseEffectLogic effectLogic, int layer, Tab_Effect data, int? handle, PlayEffectDelegate playCallback, object playParameter, object customData, float delayModifier = 0f) : base( effectLogic.transform, effectLogic.OnCreateEffect) { this.layer = layer; this.data = data; this.handle = handle; this.playCallback = playCallback; this.playParameter = playParameter; this.customData = customData; this.delayModifier = delayModifier; startTime = Time.time; } } #endregion #region 特效物品自定义参数 public class BulletCustomData : CustomEffectLoadData { public readonly Tab_Bullet bulletData; public readonly int sourceId; public readonly int targetId; public BulletCustomData(Tab_Bullet bulletData, int sourceId, int targetId) { this.bulletData = bulletData; this.sourceId = sourceId; this.targetId = targetId; } public override CommonEffectType EffectType { get { return CommonEffectType.Bullet; } } } public class BulletToPosData : CustomEffectLoadData { public readonly Tab_Bullet bulletData; public readonly Vector3 endPos; public readonly Vector3 startPos; public BulletToPosData(Tab_Bullet bulletData, Vector3 startPos, Vector3 endPos) { this.bulletData = bulletData; this.startPos = startPos; this.endPos = endPos; } public override CommonEffectType EffectType { get { return CommonEffectType.BulletToPos; } } } public class NormalEffectCustomData : CustomEffectLoadData { public readonly float duration; public readonly Vector3 position; public readonly Quaternion rotation; public NormalEffectCustomData(Vector3 position, Quaternion rotation, float duration = -1f) { this.position = position; this.rotation = rotation; this.duration = duration; } public override CommonEffectType EffectType { get { return CommonEffectType.Position; } } } public class ChainCustomData : CustomEffectLoadData { public readonly float duration; public readonly int sourceId; public readonly float speed; public ChainCustomData(int sourceId, int targetId, float duration, float speed) { this.sourceId = sourceId; TargetId = targetId; this.duration = duration; this.speed = speed; } public override CommonEffectType EffectType { get { return CommonEffectType.Chain; } } public int TargetId { get; private set; } public void ResetTarget(int targetId) { TargetId = targetId; } } public class BoundCustomData : CustomEffectLoadData { public override CommonEffectType EffectType { get { return CommonEffectType.Bound; } } } public class StampCustomData : CustomEffectLoadData { public readonly int stampCount; public StampCustomData(int stampCount) { this.stampCount = stampCount; } public override CommonEffectType EffectType { get { return CommonEffectType.Stamp; } } } public class WuChangLinkData : CustomEffectLoadData { public readonly int sourceId; public readonly int targetId; public WuChangLinkData(int sourceId, int targetId) { this.sourceId = sourceId; this.targetId = targetId; } public override CommonEffectType EffectType { get { return CommonEffectType.WuChangLink; } } } public class WuChangAirportData : CustomEffectLoadData { public readonly int count; public readonly int sourceId; public WuChangAirportData(int sourceId, int count) { this.sourceId = sourceId; this.count = count; } public override CommonEffectType EffectType { get { return CommonEffectType.WuChangAirport; } } } public class MeteorEffectData : CustomEffectLoadData { public readonly Vector3 fallPosition; public readonly float fallTime; public readonly Vector3 holdPosition; public readonly float holdTime; public readonly Vector3 rollPosition; public readonly float rollTime; public MeteorEffectData(float offsetY, float holdTime, float fallTime, float rollTime, Vector2 holdPos, Vector2 fallPos, Vector2 rollPos) { this.holdTime = holdTime; this.fallTime = fallTime + this.holdTime; this.rollTime = rollTime + this.fallTime; holdPosition = ActiveScene.GetTerrainPosition(holdPos.InsertY()); fallPosition = ActiveScene.GetTerrainPosition(fallPos.InsertY()); rollPosition = ActiveScene.GetTerrainPosition(rollPos.InsertY()); holdPosition.y += offsetY; } public override CommonEffectType EffectType { get { return CommonEffectType.Meteor; } } } public class AvatarEffectData : CustomEffectLoadData { public override CommonEffectType EffectType { get { return CommonEffectType.Avatar; } } } public abstract class CustomEffectLoadData { public abstract CommonEffectType EffectType { get; } } #endregion #region 特效物品引用参数 /// /// 标准绑定位置的特效基类 /// public abstract class BoundEffectReferenceBase : BaseEffectReference { private readonly Quaternion _localRotation; public BoundEffectReferenceBase(Transform effect, Tab_Effect data, int? effectHandle) : base(data, effect, effectHandle) { _localRotation = data.Yaw > 0 ? Quaternion.Euler(0f, data.Yaw, 0f) : Quaternion.identity; } // 特效绑定点 public Transform BindPoint { get; private set; } // 特效绑定偏移值 public Vector3 Offset { get; private set; } public void SetBindPoint(Transform bindPoint, Vector3 offset) { BindPoint = bindPoint; Offset = offset; effect.SetParent(bindPoint, false); effect.transform.localPosition = offset; effect.transform.localRotation = _localRotation; var scale = data.Scale > 0 ? data.Scale * Vector3.one : Vector3.one; effect.transform.localScale = scale; } public override bool Update() { if (base.Update()) { SetRotation(); return true; } return false; } private void SetRotation() { switch (data.RotationType) { case EffectRotationType.noRotation: // 需要校正LocalPosition到世界类型 effect.rotation = _localRotation; break; case EffectRotationType.faceCamera: // 暂时使用UiManager上面那个安全接口 var mainCamera = UIManager.Instance().GetWorldCamera(); if (mainCamera != null) { var rotation = Quaternion.LookRotation(mainCamera.transform.position - effect.position); rotation = rotation * _localRotation; effect.rotation = rotation; } break; } } } /// /// 载入中特效数据,用于存放动态加载的特效实例 /// public class CommonEffectReference : BoundEffectReferenceBase { private readonly float _endTime; public CommonEffectReference(Transform effect, Tab_Effect data, int? effectHandle, float endTime) : base(effect, data, effectHandle) { _endTime = endTime; } public override bool Update() { if (Time.time < _endTime) return base.Update(); return false; } } /// /// 子弹数据记录 /// public class BulletEffectReference : BaseEffectReference { private readonly Quaternion _localRotation; private readonly BulletSimulator _simulator; // 用于在Target析构后,重新恢复Target private readonly int _targetId; public readonly Tab_Bullet bulletData; // 抛物线子弹最大偏移位置 private Vector3 _maxPathOffset; private Vector3 _targetOffset; private Transform _targetTransform; public BulletEffectReference(Transform effect, int targetId, Obj source, Tab_Bullet bulletData, Tab_Effect data, int? effectHandle) : base(data, effect, effectHandle) { this.bulletData = bulletData; _localRotation = data.Yaw > 0 ? Quaternion.Euler(0f, data.Yaw, 0f) : Quaternion.identity; _targetId = targetId; // 提前推送目标无法获得时的攻击位置 _simulator = new BulletSimulator { targetPoint = source.transform.position + source.transform.rotation * GetStartOffset() + source.transform.forward * bulletData.MinDistance }; } public Vector3 GetStartOffset() { return new Vector3(bulletData.StartPosX, bulletData.StartPosY, bulletData.StartPosZ); } public override bool Update() { var valid = base.Update() && MoveBullet(); // 子弹消散特效 if (!valid) if (bulletData.EndEffectId > 0 && bulletData.EndEffectId != bulletData.EffectId) if (IndependentEffectManager.Instance != null) IndependentEffectManager.Instance.ShowEffect(bulletData.EndEffectId, effect.gameObject.layer, GameDefine_Globe.OBJ_TYPE.OBJ, effect.position, Quaternion.Euler(0f, effect.eulerAngles.y, 0f)); return valid; } public bool InitBullet(float delay) { ResetAttackPoint(); _simulator.duration = (_simulator.targetPoint - effect.position).RemoveY().magnitude / bulletData.Speed; var result = delay < _simulator.duration; if (result) { _simulator.startTime = Time.time - delay; _maxPathOffset = 0.5f * new Vector3(bulletData.BulletDirectionX, bulletData.BulletDirectionY, 0f) * _simulator.duration; _simulator.logicPoint = effect.position; var scale = data.Scale > 0 ? data.Scale * Vector3.one : Vector3.one; effect.localScale = scale; result = MoveBullet(); } return result; } /// /// 移动子弹位置 /// /// 子弹尚未飞行到目标位置 private bool MoveBullet() { var result = true; ResetAttackPoint(); // 计算子弹飞行时间百分比 var direction = _simulator.UpdateBullet().normalized; if (_simulator.lastRatio >= 1f) result = false; var logicPoint = _simulator.logicPoint; if (_maxPathOffset == Vector3.zero || direction == Vector3.zero) { effect.position = logicPoint; } else { var x = Vector3.Cross(Vector3.up, direction).normalized; effect.position = logicPoint + (_maxPathOffset.x * x + _maxPathOffset.y * Vector3.up) * (1f - Mathf.Abs(_simulator.lastRatio * 2 - 1f).ToSquare()); } // 调节子弹面对方向 if (direction != Vector3.zero) { var rotationType = data.RotationType; // 如果没有摄像机就统一回退到最基础方式 var cameraTrans = SceneLogic.CameraController != null && SceneLogic.CameraController.MainCamera != null ? SceneLogic.CameraController.MainCamera.transform : null; if (cameraTrans == null) rotationType = EffectRotationType.byEffectType; switch (rotationType) { case EffectRotationType.faceCamera: // 忽略这个Null警告,这个位置不可能是Null var y = cameraTrans.position - effect.position; var x = Vector3.Cross(y, direction); y = Vector3.Cross(direction, x); if (y == Vector3.zero) y = Vector3.up; effect.rotation = Quaternion.LookRotation(direction, y) * _localRotation; break; case EffectRotationType.byEffectType: effect.rotation = Quaternion.LookRotation(direction, Vector3.up) * _localRotation; break; } } return result; } /// /// 获得子弹攻击目标位置 /// private void ResetAttackPoint() { // 试图恢复目标绑定 if (_targetId != 0 && _targetTransform == null) { var targetObj = Singleton.Instance.FindObjCharacterInScene(_targetId); if (targetObj != null) if (targetObj.ObjEffectLogic == null) { _targetTransform = targetObj.transform; _targetOffset = Vector3.zero; } else { _targetTransform = targetObj.ObjEffectLogic.GetBindPoint(bulletData.AttackPoint, out _targetOffset); } } // 如果目标没有丢失,使用目标校正攻击位置 if (_targetTransform != null) _simulator.targetPoint = _targetTransform.rotation * _targetOffset + _targetTransform.position; } } /// /// 对位置飞行子弹引用记录 /// public class BulletToPosReference : BaseEffectReference { private readonly float _duration; private readonly Vector3 _endPos; private readonly Quaternion _localRotation; private readonly Vector3 _offsetDir; private readonly Vector3 _startPos; private readonly float _startTime; public readonly Tab_Bullet bulletData; public BulletToPosReference(Transform effect, Vector3 startPos, Vector3 endPos, Tab_Bullet bulletData, Tab_Effect data, int? effectHandle) : base(data, effect, effectHandle) { this.bulletData = bulletData; _startPos = startPos; _endPos = endPos; _startTime = Time.time; _localRotation = data.Yaw > 0 ? Quaternion.Euler(0f, data.Yaw, 0f) : Quaternion.identity; var delta = endPos - startPos; delta.y = 0f; if (delta == Vector3.zero) { _startTime = float.NegativeInfinity; _duration = 0f; effect.position = startPos; } else { _duration = Mathf.Min(60f, delta.magnitude / bulletData.Speed); var rotation = Quaternion.LookRotation(delta, Vector3.up) * _localRotation; _offsetDir = rotation * new Vector3(bulletData.BulletDirectionX, bulletData.BulletDirectionY, 0f); MoveBullet(); } } public override bool Update() { var valid = base.Update() && MoveBullet(); // 子弹消散特效 if (!valid) if (bulletData.EndEffectId > 0 && bulletData.EndEffectId != bulletData.EffectId) if (IndependentEffectManager.Instance != null) IndependentEffectManager.Instance.ShowEffect(bulletData.EndEffectId, effect.gameObject.layer, GameDefine_Globe.OBJ_TYPE.OBJ, effect.position, Quaternion.Euler(0f, effect.eulerAngles.y, 0f)); return valid; } /// /// 移动子弹位置 /// /// 子弹尚未飞行到目标位置 private bool MoveBullet() { var result = Time.time < _startTime + _duration; if (result) { var time = Time.time - _startTime; var ratio = time / _duration; var direction = (_endPos - _startPos).normalized; if (_offsetDir == Vector3.zero) { effect.position = Vector3.Lerp(_startPos, _endPos, ratio); } else { var currentOffsetDir = _offsetDir * (1f - ratio * 2f); effect.position = Vector3.Lerp(_startPos, _endPos, ratio) + (_offsetDir + currentOffsetDir) * 0.5f * bulletData.Speed * time; direction += currentOffsetDir; } var rotationType = data.RotationType; // 如果没有摄像机就统一回退到最基础方式 var cameraTrans = SceneLogic.CameraController != null && SceneLogic.CameraController.MainCamera != null ? SceneLogic.CameraController.MainCamera.transform : null; if (cameraTrans == null) rotationType = EffectRotationType.byEffectType; switch (rotationType) { case EffectRotationType.faceCamera: // 忽略这个Null警告,这个位置不可能是Null var y = cameraTrans.position - effect.position; var x = Vector3.Cross(y, direction); y = Vector3.Cross(direction, x); if (y == Vector3.zero) y = Vector3.up; effect.rotation = Quaternion.LookRotation(direction, y) * _localRotation; break; case EffectRotationType.byEffectType: effect.rotation = Quaternion.LookRotation(direction, Vector3.up) * _localRotation; break; } } else { effect.position = _endPos; // 特殊处理结束不消失的流程 if (bulletData.EffectId > 0 && bulletData.EffectId == bulletData.EndEffectId && _startTime + data.Duration > Time.time) result = true; } return result; } } /// /// 锁链特效引用记录 /// public class ChainEffectReference : BaseEffectReference { // 用于在Source或者Target析构后,重新恢复效果 private readonly LinkFxController _controller; // 如果锁链拥有消散时间和飞行速度,需要做子弹效果 private BulletSimulator _bulletSimulator; private ChainCustomData _chainData; private float _endTime; private Obj_Character _source; private Obj_Character _target; public ChainEffectReference(Transform effect, ChainCustomData chainData, Tab_Effect data, int? effectHandle) : base( data, effect, effectHandle) { _chainData = chainData; _controller = effect.GetComponent(); if (_controller == null) LogModule.ErrorLog(string.Format("特效物体{0}上不存在锁链控制器", data.Path)); ResetEndTime(); } public override bool Update() { var result = base.Update() && Time.time < _endTime; if (result && _controller != null) { if (_source == null || _source.ServerID != _chainData.sourceId) _source = ObjManager.Instance.FindObjCharacterInScene(_chainData.sourceId); if (_target == null || _target.ServerID != _chainData.TargetId) _target = ObjManager.Instance.FindObjCharacterInScene(_chainData.TargetId); // 需要动态初始化子弹的情况 if (_bulletSimulator == null && _source != null && _target != null) { var sourcePoint = GetChainPoint(_source, data.ParentName); var targetPoint = GetChainPoint(_target, EffectLogic.centerName); _bulletSimulator = new BulletSimulator(sourcePoint, targetPoint, _chainData.speed > 0f ? Vector3.Distance(sourcePoint, targetPoint) / _chainData.speed : 0f); } if (_bulletSimulator != null) { // 如果目标存在,刷新目标位置 if (_target != null) _bulletSimulator.targetPoint = GetChainPoint(_target, EffectLogic.centerName); _bulletSimulator.UpdateBullet(); if (_source != null) { var sourcePoint = GetChainPoint(_source, data.ParentName); _controller.sourcePoint = sourcePoint; } _controller.targetPoint = _bulletSimulator.logicPoint; _controller.TryUpdateLink(); } } return result; } public void ResetParameters(ChainCustomData chainData) { // 检查,如果Src或者Target有改变,就重置锁链 // 否则仅仅修改参数 if (chainData.sourceId != _chainData.sourceId || chainData.TargetId != _chainData.TargetId) { _source = null; _target = null; _bulletSimulator = null; ResetEndTime(); } _chainData = chainData; Update(); } private void ResetEndTime() { _endTime = _chainData.duration > 0 ? Time.time + _chainData.duration : float.PositiveInfinity; if (_controller != null) _controller.ResetEndTime(_endTime); } /// /// 恢复锁链攻击目标 /// private Vector3 GetChainPoint(Obj_Character target, string bindName) { if (target.ObjEffectLogic != null) return target.ObjEffectLogic.GetBindPointPosition(bindName); return target.Position; } } public class StampEffectReference : BoundEffectReferenceBase { private readonly MarkSfxController _controller; public StampEffectReference(Transform effect, Tab_Effect data, int initCount, int? effectHandle) : base(effect, data, effectHandle) { _controller = effect.GetComponent(); if (_controller == null) LogModule.ErrorLog(string.Format("特效物体{0}上不存在印记控制器", data.Path)); else _controller.TargetCount = initCount; } public void SetCurrentCount(int count) { _controller.TargetCount = count; } public void Detonate() { _controller.Detonate(true); } public override bool Update() { var result = base.Update(); if (result) result = !_controller.IsDetonated; return result; } } public class WuChangLinkReference : BaseEffectReference { // 用于在Source或者Target析构后,重新恢复效果 private readonly WuChangLinkController _controller; public readonly WuChangLinkData linkData; private bool _isBreak; public WuChangLinkReference(Transform effect, WuChangLinkData linkData, Tab_Effect data, int? effectHandle) : base( data, effect, effectHandle) { this.linkData = linkData; _controller = effect.GetComponent(); if (_controller == null) LogModule.ErrorLog(string.Format("特效物体{0}上不存在锁链控制器", data.Path)); else _controller.Init(data); } public override bool Update() { var result = base.Update(); if (result && _controller != null) { var sourceObj = ObjManager.Instance.FindObjInScene(linkData.sourceId); var targetObj = ObjManager.Instance.FindObjInScene(linkData.targetId); if (sourceObj != null && targetObj != null) { var sourcePos = sourceObj.ObjEffectLogic == null ? sourceObj.Position : sourceObj.ObjEffectLogic.GetBindPointPosition(EffectLogic.centerName); var targetPos = targetObj.ObjEffectLogic == null ? targetObj.Position : targetObj.ObjEffectLogic.GetBindPointPosition(EffectLogic.centerName); _controller.gameObject.SetActive(true); result = _controller.SetLinkPosition(sourcePos, targetPos, _isBreak); } else { _controller.gameObject.SetActive(false); result = false; } // 如果没有断开,则不回收;否则等待断开动画结束 result = result || !_isBreak; } return result; } public void BreakLink() { _isBreak = true; } } public class WuChangAirportReference : BaseEffectReference { // 用于在Source或者Target析构后,重新恢复效果 private readonly WuChangGhostAuraController _controller; public readonly WuChangAirportData airportData; private bool _active; public WuChangAirportReference(Transform effect, WuChangAirportData airportData, Tab_Effect data, int? effectHandle) : base( data, effect, effectHandle) { this.airportData = airportData; _active = true; _controller = effect.GetComponent(); if (_controller == null) LogModule.ErrorLog(string.Format("特效物体{0}上不存在幽灵机场控制器", data.Path)); else _controller.Init(airportData.count, data); } public override bool Update() { var result = base.Update(); if (result && _controller != null) { var sourceObj = ObjManager.Instance.FindObjInScene(airportData.sourceId); _active = sourceObj != null; _controller.gameObject.SetActive(_active); if (_active) _controller.ownerPos = sourceObj.ObjEffectLogic == null ? sourceObj.Position : sourceObj.ObjEffectLogic.GetBindPointPosition(data.ParentName); } return result; } public void AttackTarget(Transform target, Vector3 offset, float impactTime) { if (_active) _controller.AttackTarget(target, offset, impactTime); } } /// /// 陨石轨迹模拟器 /// // 陨石实际为一个三段式的子弹,三阶段都是匀速移动流程 public class MeteorEffectReference : BaseEffectReference { private readonly MeteorEffectController _meteorController; private readonly MeteorEffectData _meteorData; private readonly BulletSimulator _simulator; private readonly float _startTime; private int _groundEffectCount; private MeteorState _meteorState; public MeteorEffectReference(Transform effect, MeteorEffectData meteorData, Tab_Effect data, int? effectHandle) : base(data, effect, effectHandle) { _groundEffectCount = 0; _meteorData = meteorData; _startTime = Time.unscaledTime; _meteorState = MeteorState.Wait; var z = _meteorData.fallPosition - _meteorData.holdPosition; var x = Vector3.Cross(Vector3.up, z); var y = Vector3.Cross(z, x); if (y == Vector3.zero) y = Vector3.up; effect.rotation = Quaternion.LookRotation(z, y); _meteorController = effect.GetComponent(); var collider = effect.GetComponentInChildren(); if (collider != null) collider.gameObject.layer = ActiveScene.obstacleLayer; if (_meteorController != null) Move(); } public override bool Update() { // 只通过句柄删除 var valid = base.Update() && _meteorController != null; if (valid) Move(); return valid; } private void Move() { MeteorState nextState; if (Time.unscaledTime < _startTime + _meteorData.holdTime) nextState = MeteorState.Wait; else if (Time.unscaledTime < _startTime + _meteorData.fallTime) nextState = MeteorState.Fall; else if (Time.unscaledTime < _startTime + _meteorData.rollTime) nextState = MeteorState.Roll; else nextState = MeteorState.End; if (_meteorState != nextState) { _meteorState = nextState; //if (_meteorState == MeteorState.End) //{ // var distance = Vector3.Distance(_meteorData.rollPosition, _meteorData.fallPosition); // var pitch = distance * _distanceToPitch; // effect.rotation = _startQuaternion * Quaternion.Euler(pitch, 0f, 0f); //} if (_meteorState == MeteorState.Roll) _meteorController.SetOnGround(true); } switch (_meteorState) { case MeteorState.Wait: { effect.position = _meteorData.holdPosition; } break; case MeteorState.Fall: { var ratio = (Time.unscaledTime - _startTime - _meteorData.holdTime) / (_meteorData.fallTime - _meteorData.holdTime); effect.position = Vector3.Lerp(_meteorData.holdPosition, _meteorData.fallPosition + Vector3.up * _meteorController.radius, ratio); } break; case MeteorState.Roll: { var ratio = (Time.unscaledTime - _startTime - _meteorData.fallTime) / (_meteorData.rollTime - _meteorData.fallTime); var effectPos = Vector3.Lerp(_meteorData.fallPosition, _meteorData.rollPosition, ratio); var distance = Vector3.Distance(effect.position, _meteorData.fallPosition); _meteorController.SetRotationByDistance(distance); effect.position = effectPos + _meteorController.radius * Vector3.up; CreateGroundEffect(distance); } break; default: effect.position = _meteorData.rollPosition + _meteorController.radius * Vector3.up; break; } } private void CreateGroundEffect(float distance) { if (distance > _groundEffectCount) { // 制造第一个地面特效 var groundEffectId = data.GetParamValuebyIndex(1); if (groundEffectId >= 0) { var time = Time.unscaledDeltaTime - _startTime - _meteorData.fallTime; var groundData = CommonUtility.TryGetTable(groundEffectId, a => TableManager.GetEffectByID(a, 0)); if (groundData != null && groundData.Duration > time) IndependentEffectManager.Instance.ShowEffect(groundEffectId, effect.gameObject.layer, GameDefine_Globe.OBJ_TYPE.OBJ, effect.position - _meteorController.radius * Vector3.up, Quaternion.identity, groundData.Duration - time); } _groundEffectCount = Mathf.FloorToInt(distance) + 1; } } private enum MeteorState { Wait, // 下落前 Fall, // 下落中 Roll, // 落地后 End // 停止滚动后 } } // 背后神像效果 public class AvatarEffectReference : CommonEffectReference { private readonly AnimationLogic _effectAnim; private AnimationLogic _characterAnim; public AvatarEffectReference(Transform effect, Tab_Effect data, int? effectHandle, float endTime) : base(effect, data, effectHandle, endTime) { _effectAnim = effect.gameObject.EnsureComponent(); _effectAnim.InitAnimLogicData(effect.GetComponentInChildren(), null); _effectAnim.enabled = true; } public void Init(Obj_Character objCharacter) { // 注:Obj本身必然会在特效执行前执行事件绑定,因此特效监听到的时机必然在Obj执行过模型初始化 objCharacter.ModelNode.onModelCreate += OnCharacterModelCreate; objCharacter.ModelNode.onModelDestroy += OnCharacterModelRemove; if (objCharacter.ModelNode.model != null) OnCharacterModelCreate(objCharacter.ModelNode.model, null); effect.SetParent(objCharacter.ObjTransform); effect.localPosition = new Vector3(data.OffsetX, data.OffsetY, data.OffsetZ); effect.localScale = Vector3.one * (data.Scale > 0 ? data.Scale : 1f); effect.localRotation = Quaternion.identity; } private void OnCharacterModelCreate(ObjPartRoot partRoot, object args) { _characterAnim = partRoot.GetComponent(); if (_characterAnim != null) _characterAnim.onPlayAnim += OnCharacterPlayAnim; } private void OnCharacterModelRemove(ObjPartRoot partRoot) { if (_characterAnim != null) { _characterAnim.onPlayAnim -= OnCharacterPlayAnim; _characterAnim = null; } } public override void EndEffect() { base.EndEffect(); if (_characterAnim != null) _characterAnim.onPlayAnim -= OnCharacterPlayAnim; } private void OnCharacterPlayAnim(Tab_Animation animData, string transitionName) { if (_effectAnim != null) _effectAnim.Play(animData); } } /// /// 模拟子弹运动轨迹的模拟器 /// public class BulletSimulator { // 子弹预期命中目标时间 public float duration; // 子弹在上一帧的行走时间记录 public float lastRatio; // 子弹逻辑位置记录 - 抛物线子弹逻辑位置和显示位置不一致 public Vector3 logicPoint; // 子弹飞行开始时间 public float startTime; // 子弹目标位置 public Vector3 targetPoint; public BulletSimulator() { } public BulletSimulator(Vector3 startPoint, Vector3 targetPoint, float duration, float startTime = -1f) { this.startTime = startTime < 0f ? Time.time : startTime; this.duration = duration; logicPoint = startPoint; this.targetPoint = targetPoint; lastRatio = 0f; } public Vector3 UpdateBullet() { // 计算子弹飞行时间百分比 var ratio = duration > 0f ? (Time.time - startTime) / duration : 1f; var lastPoint = logicPoint; var direction = Vector3.zero; if (ratio >= 1f) { logicPoint = targetPoint; if (lastRatio < 1f) direction = logicPoint - lastPoint; } else { var deltaRatio = (ratio - lastRatio) / (1f - lastRatio); logicPoint = Vector3.Lerp(logicPoint, targetPoint, deltaRatio); direction = logicPoint - lastPoint; } lastRatio = ratio; return direction; } } #endregion /// /// 特效逻辑类型 /// public enum CommonEffectType { Bound, // 绑定在节点上面的类型 Position, // 按照位置特效播放的特效类型 Bullet, // 子弹类型 Chain, // 闪电链类型特效 Stamp, // 印记类型特效 WuChangLink, // 无常锁链特效 WuChangAirport, // 无常幽灵机场特效 Meteor, // 陨石类型特效 Avatar, // 背后神象效果 BulletToPos // 对位置攻击的子弹 } /// /// 特效旋转策略类型 /// public static class EffectRotationType { public const int noRotation = 0; public const int faceCamera = 1; public const int byEffectType = 2; }