// MIT License - Copyright (c) 2025 wallstop
// Full license text: https://github.com/wallstop/unity-helpers/blob/main/LICENSE
namespace WallstopStudios.UnityHelpers.Tags
{
using System;
using System.Collections.Generic;
using Core.Attributes;
using Core.Extension;
using Core.Helper;
using UnityEngine;
using Utils;
// ReSharper disable once GrammarMistakeInComment
///
/// Manages the application and removal of AttributeEffects on a GameObject.
/// Handles effect duration tracking, tag application, cosmetic effects, and attribute modifications.
///
///
///
/// The EffectHandler is the central component for the effect system. It:
/// - Applies effects and creates handles for tracking
/// - Manages effect durations and automatic expiration
/// - Coordinates with TagHandler for tag-based effects
/// - Manages cosmetic effect instantiation and cleanup
/// - Distributes attribute modifications to AttributesComponents
///
///
/// Example usage:
///
/// var effectHandler = gameObject.GetComponent<EffectHandler>();
///
/// // Apply an effect
/// AttributeEffect poisonEffect = ...;
/// EffectHandle? handle = effectHandler.ApplyEffect(poisonEffect);
///
/// // Remove a specific effect
/// if (handle.HasValue)
/// {
/// effectHandler.RemoveEffect(handle.Value);
/// }
///
/// // Remove all effects
/// effectHandler.RemoveAllEffects();
///
///
///
[DisallowMultipleComponent]
[RequireComponent(typeof(TagHandler))]
public sealed class EffectHandler : MonoBehaviour
{
///
/// Invoked when an effect is successfully applied.
///
public event Action OnEffectApplied;
///
/// Invoked when an effect is removed (either manually or through expiration).
///
public event Action OnEffectRemoved;
[SiblingComponent]
#pragma warning disable CS0649 // Field is never assigned to, and will always have its default value
private TagHandler _tagHandler;
#pragma warning restore CS0649 // Field is never assigned to, and will always have its default value
[SiblingComponent(Optional = true)]
private HashSet _attributes;
// Stores instanced cosmetic effect data for associated effects.
private readonly Dictionary<
EffectHandle,
PooledResource>
> _instancedCosmeticEffects = new();
private readonly Dictionary<
EffectStackKey,
PooledResource>
> _handlesByStackKey = new();
private readonly Dictionary _stackKeyByHandleId = new();
// Stores expiration time of duration effects (We store by Id because it's much cheaper to iterate Guids than it is EffectHandles
private readonly Dictionary _effectExpirations = new();
private readonly Dictionary _effectHandlesById = new();
// Used only to save allocations in Update()
private readonly List _expiredEffectIds = new();
private readonly List _appliedEffects = new();
private readonly Dictionary<
long,
PooledResource>
> _periodicEffectStates = new();
private readonly Dictionary<
long,
PooledResource>
> _behaviorsByHandleId = new();
private bool _initialized;
private void Awake()
{
this.AssignRelationalComponents();
_initialized = true;
}
///
/// Registers an AttributesComponent to receive effect modifications.
/// Called automatically by AttributesComponent during Awake.
///
/// The component to register.
internal void Register(AttributesComponent attributesComponent)
{
_attributes ??= new HashSet();
_ = _attributes.Add(attributesComponent);
}
///
/// Unregisters an AttributesComponent from receiving effect modifications.
/// Called automatically by AttributesComponent during OnDestroy.
///
/// The component to unregister.
internal void Remove(AttributesComponent attributesComponent)
{
_attributes?.Remove(attributesComponent);
}
///
/// Applies an AttributeEffect to this GameObject, handling tags, cosmetic effects, and attribute modifications.
///
/// The effect to apply.
///
/// An EffectHandle if the effect is non-instant (Duration or Infinite), allowing later removal.
/// Null for instant effects that permanently modify base values.
///
///
/// For Duration effects with the same name, reapplying can either reset the timer (if resetDurationOnReapplication is true)
/// or be ignored if already active.
///
public EffectHandle? ApplyEffect(AttributeEffect effect)
{
if (effect == null)
{
return null;
}
if (effect.durationType == ModifierDurationType.Instant)
{
if (RequiresHandle(effect))
{
this.LogWarn(
$"Effect {effect:json} defines periodic or behaviour data but is Instant. These features require a Duration or Infinite effect."
);
}
InternalApplyEffect(effect);
return null;
}
EffectStackKey stackKey = effect.GetStackKey();
List existingHandles = TryGetStackHandles(stackKey);
switch (effect.stackingMode)
{
case EffectStackingMode.Ignore:
{
if (existingHandles is { Count: > 0 })
{
return existingHandles[0];
}
break;
}
case EffectStackingMode.Refresh:
{
if (existingHandles is { Count: > 0 })
{
EffectHandle handle = existingHandles[0];
InternalApplyEffect(handle);
return handle;
}
break;
}
case EffectStackingMode.Replace:
{
if (existingHandles is { Count: > 0 })
{
using PooledResource> handleBufferResource =
Buffers.List.Get(out List handleBuffer);
handleBuffer.AddRange(existingHandles);
foreach (EffectHandle handle in handleBuffer)
{
RemoveEffect(handle);
}
}
break;
}
case EffectStackingMode.Stack:
{
if (existingHandles is { Count: > 0 } && effect.maximumStacks > 0)
{
while (existingHandles.Count >= effect.maximumStacks)
{
EffectHandle oldestHandle = existingHandles[0];
RemoveEffect(oldestHandle);
}
}
break;
}
}
EffectHandle newHandle = EffectHandle.CreateInstance(effect);
RegisterStackHandle(stackKey, newHandle);
InternalApplyEffect(newHandle);
return newHandle;
}
private static bool RequiresHandle(AttributeEffect effect)
{
return (effect.periodicEffects is { Count: > 0 })
|| (effect.behaviors is { Count: > 0 });
}
private List TryGetStackHandles(EffectStackKey stackKey)
{
return _handlesByStackKey.TryGetValue(
stackKey,
out PooledResource> lease
)
? lease.resource
: null;
}
private void RegisterStackHandle(EffectStackKey stackKey, EffectHandle handle)
{
long handleId = handle.id;
_stackKeyByHandleId[handleId] = stackKey;
List handles;
if (
!_handlesByStackKey.TryGetValue(
stackKey,
out PooledResource> handlesLease
)
)
{
handlesLease = RentHandleList(out handles);
_handlesByStackKey.Add(stackKey, handlesLease);
}
else
{
handles = handlesLease.resource;
}
handles.Add(handle);
}
///
/// Removes a specific effect by its handle, cleaning up tags, cosmetic effects, and attribute modifications.
///
/// The handle of the effect to remove.
public void RemoveEffect(EffectHandle handle)
{
InternalRemoveEffect(handle);
_ = _appliedEffects.Remove(handle);
DeregisterHandle(handle);
}
public void RemoveAllEffects()
{
using PooledResource> handleBufferResource =
Buffers.List.Get(out List handleBuffer);
handleBuffer.AddRange(_appliedEffects);
foreach (EffectHandle handle in handleBuffer)
{
RemoveEffect(handle);
}
_appliedEffects.Clear();
}
private void OnDestroy()
{
RemoveAllEffects();
if (_handlesByStackKey.Count > 0)
{
using PooledResource> stackKeysResource =
Buffers.List.Get(out List stackKeys);
stackKeys.AddRange(_handlesByStackKey.Keys);
foreach (EffectStackKey stackKey in stackKeys)
{
if (
_handlesByStackKey.TryGetValue(
stackKey,
out PooledResource> lease
)
)
{
ClearAndDispose(lease);
}
}
_handlesByStackKey.Clear();
_stackKeyByHandleId.Clear();
}
foreach (
KeyValuePair<
EffectHandle,
PooledResource>
> cosmetic in _instancedCosmeticEffects
)
{
RecycleCosmeticDataList(cosmetic.Value);
}
_instancedCosmeticEffects.Clear();
foreach (
KeyValuePair<
long,
PooledResource>
> periodic in _periodicEffectStates
)
{
RecyclePeriodicStateList(periodic.Value);
}
_periodicEffectStates.Clear();
foreach (
KeyValuePair<
long,
PooledResource>
> behavior in _behaviorsByHandleId
)
{
RecycleBehaviorList(behavior.Value);
}
_behaviorsByHandleId.Clear();
_effectExpirations.Clear();
_effectHandlesById.Clear();
_expiredEffectIds.Clear();
_appliedEffects.Clear();
}
private void DeregisterHandle(EffectHandle handle)
{
long handleId = handle.id;
if (_stackKeyByHandleId.TryGetValue(handleId, out EffectStackKey stackKey))
{
if (
_handlesByStackKey.TryGetValue(
stackKey,
out PooledResource> handlesLease
)
)
{
List handles = handlesLease.resource;
handles.Remove(handle);
if (handles.Count == 0)
{
_handlesByStackKey.Remove(stackKey);
ClearAndDispose(handlesLease);
}
}
_stackKeyByHandleId.Remove(handleId);
}
if (
_periodicEffectStates.Remove(
handleId,
out PooledResource> periodicLease
)
)
{
RecyclePeriodicStateList(periodicLease);
}
if (
_behaviorsByHandleId.Remove(
handleId,
out PooledResource> behaviorLease
)
)
{
List behaviorInstances = behaviorLease.resource;
EffectBehaviorContext context = new(this, handle, 0f);
foreach (EffectBehavior behavior in behaviorInstances)
{
if (behavior == null)
{
continue;
}
behavior.OnRemove(context);
Destroy(behavior);
}
RecycleBehaviorList(behaviorLease);
}
}
///
/// Determines whether the specified effect is currently active on this handler.
///
/// The effect to check.
/// true if at least one handle for the effect is active; otherwise, false.
public bool IsEffectActive(AttributeEffect effect)
{
if (effect == null)
{
return false;
}
foreach (EffectHandle handle in _appliedEffects)
{
if (handle.effect == effect)
{
return true;
}
}
return false;
}
///
/// Gets the number of active handles for the specified effect.
///
/// The effect to count.
/// The number of active handles associated with .
public int GetEffectStackCount(AttributeEffect effect)
{
if (effect == null)
{
return 0;
}
int count = 0;
foreach (EffectHandle handle in _appliedEffects)
{
if (handle.effect == effect)
{
++count;
}
}
return count;
}
///
/// Copies all active effect handles into the provided buffer.
///
///
/// Optional list to populate. When null, a new list is created. The buffer is cleared before population.
///
/// The populated buffer containing all currently active effect handles.
public List GetActiveEffects(List buffer = null)
{
buffer ??= new List();
buffer.Clear();
buffer.AddRange(_appliedEffects);
return buffer;
}
///
/// Attempts to retrieve the remaining duration for the specified effect handle.
///
/// The handle to inspect.
/// When this method returns, contains the remaining time in seconds, or zero if unavailable.
/// true if the handle has a tracked duration; otherwise, false.
public bool TryGetRemainingDuration(EffectHandle handle, out float remainingDuration)
{
long handleId = handle.id;
if (!_effectExpirations.TryGetValue(handleId, out float expiration))
{
remainingDuration = 0f;
return false;
}
float timeRemaining = expiration - Time.time;
if (timeRemaining < 0f)
{
timeRemaining = 0f;
}
remainingDuration = timeRemaining;
return true;
}
///
/// Ensures an effect handle exists for the specified effect, optionally refreshing its duration if already active.
///
/// The effect to apply or refresh.
/// An active handle for the effect, or null for instant effects.
public EffectHandle? EnsureHandle(AttributeEffect effect)
{
return EnsureHandle(effect, refreshDuration: true);
}
///
/// Ensures an effect handle exists for the specified effect, optionally refreshing its duration if already active.
///
/// The effect to apply or refresh.
///
/// When true, attempts to refresh the effect's duration when it is already active and supports reapplication.
///
/// An active handle for the effect, or null for instant effects.
public EffectHandle? EnsureHandle(AttributeEffect effect, bool refreshDuration)
{
if (effect == null)
{
return null;
}
foreach (EffectHandle handle in _appliedEffects)
{
if (handle.effect == effect)
{
if (refreshDuration)
{
_ = RefreshEffect(handle);
}
return handle;
}
}
return ApplyEffect(effect);
}
///
/// Attempts to refresh the duration of the specified effect handle.
///
/// The handle to refresh.
/// true if the duration was refreshed; otherwise, false.
public bool RefreshEffect(EffectHandle handle)
{
return RefreshEffect(handle, ignoreReapplicationPolicy: false);
}
///
/// Attempts to refresh the duration of the specified effect handle.
///
/// The handle to refresh.
///
/// When true, refreshes the duration even if is false.
///
/// true if the duration was refreshed; otherwise, false.
public bool RefreshEffect(EffectHandle handle, bool ignoreReapplicationPolicy)
{
AttributeEffect effect = handle.effect;
if (effect == null)
{
return false;
}
if (effect.durationType != ModifierDurationType.Duration)
{
return false;
}
if (!ignoreReapplicationPolicy && !effect.resetDurationOnReapplication)
{
return false;
}
long handleId = handle.id;
if (!_effectExpirations.ContainsKey(handleId))
{
return false;
}
float newExpiration = Time.time + effect.duration;
_effectExpirations[handleId] = newExpiration;
_effectHandlesById[handleId] = handle;
return true;
}
private void InternalRemoveEffect(EffectHandle handle)
{
if (_attributes != null)
{
foreach (AttributesComponent attributesComponent in _attributes)
{
attributesComponent.ForceRemoveAttributeModifications(handle);
}
}
if (!_initialized && _tagHandler == null)
{
this.AssignRelationalComponents();
}
// Then, tags are removed (so cosmetic components can look up if any tags are still applied)
if (_tagHandler != null)
{
_ = _tagHandler.ForceRemoveTags(handle);
}
long handleId = handle.id;
_ = _effectExpirations.Remove(handleId);
_ = _effectHandlesById.Remove(handleId);
InternalRemoveCosmeticEffects(handle);
OnEffectRemoved?.Invoke(handle);
}
private void InternalApplyEffect(EffectHandle handle)
{
bool exists = _appliedEffects.Contains(handle);
if (!exists)
{
_appliedEffects.Add(handle);
}
long handleId = handle.id;
_effectHandlesById[handleId] = handle;
AttributeEffect effect = handle.effect;
if (effect.durationType == ModifierDurationType.Duration)
{
if (!exists || effect.resetDurationOnReapplication)
{
_effectExpirations[handleId] = Time.time + effect.duration;
}
}
if (!exists)
{
RegisterPeriodicRuntime(handle);
RegisterBehaviors(handle);
}
if (!_initialized && _tagHandler == null)
{
this.AssignRelationalComponents();
}
if (_tagHandler != null && effect.effectTags is { Count: > 0 })
{
_tagHandler.ForceApplyTags(handle);
}
if (effect.cosmeticEffects is { Count: > 0 })
{
InternalApplyCosmeticEffects(handle);
}
if (effect.modifications is { Count: > 0 })
{
foreach (AttributesComponent attributesComponent in _attributes)
{
attributesComponent.ForceApplyAttributeModifications(handle);
}
}
OnEffectApplied?.Invoke(handle);
}
private void InternalApplyEffect(AttributeEffect effect)
{
if (effect.durationType == ModifierDurationType.Instant && RequiresHandle(effect))
{
this.LogWarn(
$"Effect {effect:json} defines periodic or behaviour data but is Instant. These features require a Duration or Infinite effect."
);
}
if (!_initialized && _tagHandler == null)
{
this.AssignRelationalComponents();
}
if (_tagHandler != null && effect.effectTags is { Count: > 0 })
{
_tagHandler.ForceApplyEffect(effect);
}
if (effect.cosmeticEffects is { Count: > 0 })
{
InternalApplyCosmeticEffects(effect);
}
if (effect.modifications is { Count: > 0 })
{
foreach (AttributesComponent attributesComponent in _attributes)
{
attributesComponent.ForceApplyAttributeModifications(effect);
}
}
}
private void RegisterPeriodicRuntime(EffectHandle handle)
{
AttributeEffect effect = handle.effect;
if (effect.periodicEffects is not { Count: > 0 })
{
return;
}
List runtimeStates = null;
PooledResource> runtimeStatesLease = default;
float startTime = Time.time;
foreach (PeriodicEffectDefinition definition in effect.periodicEffects)
{
if (definition == null)
{
continue;
}
if (runtimeStates == null)
{
runtimeStatesLease = RentPeriodicStateList(out runtimeStates);
}
runtimeStates.Add(new PeriodicEffectRuntimeState(definition, startTime));
}
if (runtimeStates is { Count: > 0 })
{
_periodicEffectStates[handle.id] = runtimeStatesLease;
}
else if (runtimeStates != null)
{
RecyclePeriodicStateList(runtimeStatesLease);
}
}
private void RegisterBehaviors(EffectHandle handle)
{
AttributeEffect effect = handle.effect;
if (effect.behaviors is not { Count: > 0 })
{
return;
}
List instances = null;
PooledResource> instancesLease = default;
foreach (EffectBehavior behavior in effect.behaviors)
{
if (behavior == null)
{
continue;
}
EffectBehavior clone = Instantiate(behavior);
if (instances == null)
{
instancesLease = RentBehaviorList(out instances);
}
instances.Add(clone);
}
if (instances is not { Count: > 0 })
{
if (instances != null)
{
RecycleBehaviorList(instancesLease);
}
return;
}
EffectBehaviorContext context = new(this, handle, 0f);
foreach (EffectBehavior instance in instances)
{
if (instance == null)
{
continue;
}
instance.OnApply(context);
}
_behaviorsByHandleId[handle.id] = instancesLease;
}
private void ApplyPeriodicTick(
EffectHandle handle,
PeriodicEffectRuntimeState runtimeState,
float currentTime,
float deltaTime
)
{
PeriodicEffectDefinition definition = runtimeState.definition;
if (_attributes is { Count: > 0 } && definition.modifications is { Count: > 0 })
{
foreach (AttributesComponent attributesComponent in _attributes)
{
attributesComponent.ApplyAttributeModifications(definition.modifications, null);
}
}
if (
_behaviorsByHandleId.TryGetValue(
handle.id,
out PooledResource> behaviorLease
)
)
{
List behaviors = behaviorLease.resource;
if (behaviors.Count == 0)
{
return;
}
EffectBehaviorContext context = new(this, handle, deltaTime);
PeriodicEffectTickContext tickContext = new(
definition,
runtimeState.ExecutedTicks,
currentTime
);
foreach (EffectBehavior behavior in behaviors)
{
if (behavior == null)
{
continue;
}
behavior.OnPeriodicTick(context, tickContext);
}
}
}
private void InternalApplyCosmeticEffects(EffectHandle handle)
{
if (_instancedCosmeticEffects.ContainsKey(handle))
{
return;
}
List instancedCosmeticData = null;
PooledResource> instancedCosmeticLease = default;
AttributeEffect effect = handle.effect;
foreach (CosmeticEffectData cosmeticEffectData in effect.cosmeticEffects)
{
CosmeticEffectData cosmeticEffect = cosmeticEffectData;
if (cosmeticEffect == null)
{
this.LogError(
$"CosmeticEffectData is null for effect {effect:json}, cannot determine instancing scheme."
);
continue;
}
if (cosmeticEffectData.RequiresInstancing)
{
cosmeticEffect = Instantiate(
cosmeticEffectData,
transform.position,
Quaternion.identity
);
cosmeticEffect.transform.SetParent(transform, true);
if (instancedCosmeticData == null)
{
instancedCosmeticLease = RentCosmeticDataList(out instancedCosmeticData);
}
instancedCosmeticData.Add(cosmeticEffect);
}
using PooledResource> cosmeticEffectsResource =
Buffers.List.Get(
out List cosmeticEffectsBuffer
);
cosmeticEffect.GetComponents(cosmeticEffectsBuffer);
foreach (CosmeticEffectComponent cosmeticComponent in cosmeticEffectsBuffer)
{
cosmeticComponent.OnApplyEffect(gameObject);
}
}
if (instancedCosmeticData != null)
{
if (instancedCosmeticData.Count > 0)
{
_instancedCosmeticEffects.Add(handle, instancedCosmeticLease);
}
else
{
RecycleCosmeticDataList(instancedCosmeticLease);
}
}
}
private void InternalApplyCosmeticEffects(AttributeEffect attributeEffect)
{
foreach (CosmeticEffectData cosmeticEffectData in attributeEffect.cosmeticEffects)
{
if (cosmeticEffectData == null)
{
this.LogError(
$"CosmeticEffectData is null for effect {attributeEffect:json}, cannot determine instancing scheme."
);
continue;
}
if (cosmeticEffectData.RequiresInstancing)
{
this.LogError(
$"CosmeticEffectData requires instancing, but can't instance (no handle)."
);
continue;
}
using PooledResource> cosmeticEffectsResource =
Buffers.List.Get(
out List cosmeticEffectsBuffer
);
cosmeticEffectData.GetComponents(cosmeticEffectsBuffer);
foreach (CosmeticEffectComponent cosmeticComponent in cosmeticEffectsBuffer)
{
cosmeticComponent.OnApplyEffect(gameObject);
}
}
}
private void InternalRemoveCosmeticEffects(EffectHandle handle)
{
if (
!_instancedCosmeticEffects.TryGetValue(
handle,
out PooledResource> cosmeticLease
)
)
{
if (handle.effect?.cosmeticEffects == null)
{
return;
}
// If we don't have instanced cosmetic effects, then they were applied directly to the cosmetic data
foreach (CosmeticEffectData cosmeticEffectData in handle.effect.cosmeticEffects)
{
if (cosmeticEffectData.RequiresInstancing)
{
this.LogWarn(
$"Double-deregistration detected for handle {handle:json}. Existing handles: [{string.Join(",", _instancedCosmeticEffects.Keys)}]."
);
continue;
}
using PooledResource> cosmeticEffectsResource =
Buffers.List.Get(
out List cosmeticEffectsBuffer
);
cosmeticEffectData.GetComponents(cosmeticEffectsBuffer);
foreach (CosmeticEffectComponent cosmeticComponent in cosmeticEffectsBuffer)
{
cosmeticComponent.OnRemoveEffect(gameObject);
}
}
return;
}
List cosmeticDatas = cosmeticLease.resource;
foreach (CosmeticEffectData cosmeticData in cosmeticDatas)
{
using PooledResource> cosmeticEffectsResource =
Buffers.List.Get(
out List cosmeticEffectsBuffer
);
cosmeticData.GetComponents(cosmeticEffectsBuffer);
foreach (CosmeticEffectComponent cosmeticComponent in cosmeticEffectsBuffer)
{
cosmeticComponent.OnRemoveEffect(gameObject);
}
}
foreach (CosmeticEffectData data in cosmeticDatas)
{
bool shouldDestroyGameObject = true;
using PooledResource> cosmeticEffectsResource =
Buffers.List.Get(
out List cosmeticEffectsBuffer
);
data.GetComponents(cosmeticEffectsBuffer);
foreach (CosmeticEffectComponent cosmeticEffect in cosmeticEffectsBuffer)
{
if (cosmeticEffect.CleansUpSelf)
{
shouldDestroyGameObject = false;
continue;
}
cosmeticEffect.Destroy();
}
if (shouldDestroyGameObject)
{
data.gameObject.Destroy();
}
}
_ = _instancedCosmeticEffects.Remove(handle);
RecycleCosmeticDataList(cosmeticLease);
}
private static PooledResource> RentHandleList(
out List handles
)
{
return Buffers.List.Get(out handles);
}
private static PooledResource> RentBehaviorList(
out List behaviors
)
{
return Buffers.List.Get(out behaviors);
}
private static PooledResource> RentPeriodicStateList(
out List states
)
{
return Buffers.List.Get(out states);
}
private static PooledResource> RentCosmeticDataList(
out List cosmeticData
)
{
return Buffers.List.Get(out cosmeticData);
}
private static void ClearAndDispose(PooledResource> lease)
{
List list = lease.resource;
list?.Clear();
lease.Dispose();
}
private static void RecycleBehaviorList(PooledResource> lease)
{
ClearAndDispose(lease);
}
private static void RecyclePeriodicStateList(
PooledResource> lease
)
{
ClearAndDispose(lease);
}
private static void RecycleCosmeticDataList(
PooledResource> cosmeticLease
)
{
List cosmeticData = cosmeticLease.resource;
if (cosmeticData != null)
{
for (int i = cosmeticData.Count - 1; i >= 0; --i)
{
cosmeticData[i] = null;
}
cosmeticData.Clear();
}
cosmeticLease.Dispose();
}
private void Update()
{
ProcessEffectExpirations();
ProcessBehaviorTicks();
ProcessPeriodicEffects();
}
private void ProcessEffectExpirations()
{
if (_effectExpirations.Count <= 0)
{
return;
}
_expiredEffectIds.Clear();
float currentTime = Time.time;
foreach (KeyValuePair entry in _effectExpirations)
{
if (entry.Value <= currentTime)
{
_expiredEffectIds.Add(entry.Key);
}
}
foreach (long expiredHandleId in _expiredEffectIds)
{
if (_effectHandlesById.TryGetValue(expiredHandleId, out EffectHandle expiredHandle))
{
RemoveEffect(expiredHandle);
}
}
_expiredEffectIds.Clear();
}
private void ProcessBehaviorTicks()
{
if (_behaviorsByHandleId.Count <= 0)
{
return;
}
using PooledResource> behaviorHandleIdsResource = Buffers.List.Get(
out List behaviorHandleIdsBuffer
);
behaviorHandleIdsBuffer.AddRange(_behaviorsByHandleId.Keys);
float deltaTime = Time.deltaTime;
foreach (long handleId in behaviorHandleIdsBuffer)
{
if (!_effectHandlesById.TryGetValue(handleId, out EffectHandle handle))
{
continue;
}
if (
!_behaviorsByHandleId.TryGetValue(
handleId,
out PooledResource> behaviorLease
)
)
{
continue;
}
List behaviors = behaviorLease.resource;
EffectBehaviorContext context = new(this, handle, deltaTime);
foreach (EffectBehavior behavior in behaviors)
{
if (behavior == null)
{
continue;
}
behavior.OnTick(context);
}
}
}
private void ProcessPeriodicEffects()
{
if (_periodicEffectStates.Count <= 0)
{
return;
}
float currentTime = Time.time;
float deltaTime = Time.deltaTime;
using PooledResource> periodicRemovalResource = Buffers.List.Get(
out List periodicRemovalBuffer
);
using PooledResource> periodHandleIdsResource = Buffers.List.Get(
out List periodicHandleIdsBuffer
);
periodicHandleIdsBuffer.AddRange(_periodicEffectStates.Keys);
foreach (long handleId in periodicHandleIdsBuffer)
{
if (!_effectHandlesById.TryGetValue(handleId, out EffectHandle handle))
{
periodicRemovalBuffer.Add(handleId);
continue;
}
if (
!_periodicEffectStates.TryGetValue(
handleId,
out PooledResource> runtimesLease
)
)
{
continue;
}
List runtimes = runtimesLease.resource;
bool hasActive = false;
foreach (PeriodicEffectRuntimeState runtimeState in runtimes)
{
if (runtimeState == null)
{
continue;
}
while (runtimeState.TryConsumeTick(currentTime))
{
ApplyPeriodicTick(handle, runtimeState, currentTime, deltaTime);
}
if (!runtimeState.IsComplete)
{
hasActive = true;
}
}
if (!hasActive)
{
periodicRemovalBuffer.Add(handleId);
}
}
foreach (long periodicHandleId in periodicRemovalBuffer)
{
if (
_periodicEffectStates.Remove(
periodicHandleId,
out PooledResource> lease
)
)
{
RecyclePeriodicStateList(lease);
}
}
}
}
}