원클릭으로
tools-unity-gameplay-ability-system
// Gameplay Ability System patterns for abilities, effects, attributes, and tags including recursion safety and lifecycle management.
// Gameplay Ability System patterns for abilities, effects, attributes, and tags including recursion safety and lifecycle management.
Mobile-specific Unity optimization patterns for memory, battery, thermal, and performance.
Unity Addressables patterns for asset loading, memory management, reference counting, and remote content delivery.
Unity animation patterns including Animancer, state machines, and performance optimization.
Behavior Designer patterns for AI behavior trees including task creation, shared variables, conditionals, and debugging.
Unity Cinemachine camera system patterns including virtual cameras, blending, and state-driven cameras.
FlowCanvas visual scripting patterns for abilities, custom nodes, and graph execution.
| name | tools-unity-gameplay-ability-system |
| description | Gameplay Ability System patterns for abilities, effects, attributes, and tags including recursion safety and lifecycle management. |
The Gameplay Ability System provides a framework for abilities, effects, attributes, and gameplay tags. This skill covers safe implementation patterns and common pitfalls.
AbilitySystemComponent (ASC)
├── Granted Abilities[]
├── Active Effects[]
├── Attribute Sets[]
└── Owned Tags[]
public class AbilitySystemComponent : MonoBehaviour
{
private List<GameplayAbilitySpec> _grantedAbilities = new();
private List<ActiveGameplayEffect> _activeEffects = new();
private Dictionary<string, AttributeSet> _attributeSets = new();
private GameplayTagContainer _ownedTags = new();
public IReadOnlyList<GameplayAbilitySpec> GrantedAbilities => _grantedAbilities;
public IReadOnlyList<ActiveGameplayEffect> ActiveEffects => _activeEffects;
public GameplayTagContainer OwnedTags => _ownedTags;
}
[CreateAssetMenu(menuName = "GAS/Ability")]
public class GameplayAbilitySO : ScriptableObject
{
[SerializeField] private string _abilityId;
[SerializeField] private GameplayTagContainer _abilityTags;
[SerializeField] private GameplayTagContainer _activationBlockedTags;
[SerializeField] private GameplayTagContainer _activationRequiredTags;
[SerializeField] private GameplayTagContainer _cancelAbilitiesWithTags;
[SerializeField] private float _cooldownDuration;
[SerializeField] private GameplayEffectSO[] _costEffects;
[SerializeField] private GameplayEffectSO[] _applyEffects;
public bool CanActivate(AbilitySystemComponent asc)
{
// Check cooldown
if (asc.HasActiveEffectWithTag(_cooldownTag))
return false;
// Check blocking tags
if (asc.OwnedTags.HasAny(_activationBlockedTags))
return false;
// Check required tags
if (!asc.OwnedTags.HasAll(_activationRequiredTags))
return false;
// Check cost
return CanPayCost(asc);
}
}
public class AbilitySystemComponent
{
public bool TryActivateAbility(GameplayAbilitySO ability)
{
if (!ability.CanActivate(this))
return false;
// Cancel conflicting abilities
CancelAbilitiesWithTags(ability.CancelAbilitiesWithTags);
// Pay cost
ApplyCostEffects(ability);
// Apply cooldown
ApplyCooldown(ability);
// Execute ability
var spec = new GameplayAbilitySpec(ability, this);
_activeAbilities.Add(spec);
spec.Activate();
return true;
}
public void EndAbility(GameplayAbilitySpec spec)
{
spec.End();
_activeAbilities.Remove(spec);
}
}
[CreateAssetMenu(menuName = "GAS/Effect")]
public class GameplayEffectSO : ScriptableObject
{
[SerializeField] private string _effectId;
[SerializeField] private GameplayEffectDuration _durationType;
[SerializeField] private float _duration;
[SerializeField] private GameplayEffectStacking _stackingPolicy;
[SerializeField] private int _maxStacks;
[SerializeField] private AttributeModifier[] _modifiers;
[SerializeField] private GameplayTagContainer _grantedTags;
[SerializeField] private GameplayEffectSO[] _conditionalEffects;
[SerializeField] private GameplayTagContainer _applicationRequiredTags;
[SerializeField] private GameplayTagContainer _removalTags;
}
public enum GameplayEffectDuration
{
Instant,
HasDuration,
Infinite
}
public enum GameplayEffectStacking
{
None,
AggregateBySource,
AggregateByTarget
}
public class AbilitySystemComponent
{
private const int MaxRecursionDepth = 16;
private int _effectApplicationDepth = 0;
public GameplayEffectHandle ApplyEffectToSelf(GameplayEffectSO effect, GameplayEffectContext context)
{
return ApplyEffectToTarget(this, effect, context);
}
public GameplayEffectHandle ApplyEffectToTarget(
AbilitySystemComponent target,
GameplayEffectSO effect,
GameplayEffectContext context)
{
// CRITICAL: Recursion guard
if (_effectApplicationDepth >= MaxRecursionDepth)
{
Debug.LogError($"Effect recursion limit reached! Effect: {effect.name}");
return GameplayEffectHandle.Invalid;
}
_effectApplicationDepth++;
try
{
// Check application requirements
if (!CanApplyEffect(target, effect))
return GameplayEffectHandle.Invalid;
// Handle stacking
var existingEffect = FindExistingEffect(target, effect);
if (existingEffect != null)
{
return HandleStacking(target, existingEffect, effect, context);
}
// Create new effect instance
var activeEffect = new ActiveGameplayEffect(effect, context, Time.time);
target._activeEffects.Add(activeEffect);
// Apply modifiers
ApplyModifiers(target, activeEffect);
// Grant tags
target._ownedTags.AddTags(effect.GrantedTags);
// Apply conditional effects (recursive, but guarded)
ApplyConditionalEffects(target, effect, context);
return activeEffect.Handle;
}
finally
{
_effectApplicationDepth--;
}
}
private void ApplyConditionalEffects(
AbilitySystemComponent target,
GameplayEffectSO effect,
GameplayEffectContext context)
{
if (effect.ConditionalEffects == null)
return;
foreach (var conditionalEffect in effect.ConditionalEffects)
{
// Each conditional effect goes through the same guarded path
ApplyEffectToTarget(target, conditionalEffect, context);
}
}
}
public class AbilitySystemComponent
{
public void RemoveEffect(GameplayEffectHandle handle)
{
var effect = _activeEffects.FirstOrDefault(e => e.Handle == handle);
if (effect == null)
return;
// Remove modifiers
RemoveModifiers(effect);
// Remove granted tags
_ownedTags.RemoveTags(effect.Definition.GrantedTags);
// Remove from list
_activeEffects.Remove(effect);
// Return to pool if using pooling
effect.Dispose();
}
public void RemoveEffectsWithTag(GameplayTag tag)
{
// Collect first to avoid modification during iteration
var toRemove = _activeEffects
.Where(e => e.Definition.EffectTags.HasTag(tag))
.Select(e => e.Handle)
.ToList();
foreach (var handle in toRemove)
{
RemoveEffect(handle);
}
}
public void RemoveAllEffects()
{
// Create copy to avoid modification during iteration
var handles = _activeEffects.Select(e => e.Handle).ToList();
foreach (var handle in handles)
{
RemoveEffect(handle);
}
}
}
public class AbilitySystemComponent
{
private void Update()
{
UpdateActiveEffects(Time.deltaTime);
}
private void UpdateActiveEffects(float deltaTime)
{
// Use reverse iteration for safe removal
for (int i = _activeEffects.Count - 1; i >= 0; i--)
{
var effect = _activeEffects[i];
// Check duration
if (effect.Definition.DurationType == GameplayEffectDuration.HasDuration)
{
effect.RemainingDuration -= deltaTime;
if (effect.RemainingDuration <= 0)
{
RemoveEffect(effect.Handle);
continue;
}
}
// Check removal tags
if (_ownedTags.HasAny(effect.Definition.RemovalTags))
{
RemoveEffect(effect.Handle);
continue;
}
// Apply periodic effects
if (effect.Definition.IsPeriodic)
{
effect.PeriodTimer -= deltaTime;
if (effect.PeriodTimer <= 0)
{
ExecutePeriodicEffect(effect);
effect.PeriodTimer = effect.Definition.Period;
}
}
}
}
}
public class AttributeSet
{
private Dictionary<string, GameplayAttribute> _attributes = new();
public float GetValue(string attributeName)
{
if (_attributes.TryGetValue(attributeName, out var attr))
return attr.CurrentValue;
return 0f;
}
public void SetBaseValue(string attributeName, float value)
{
if (_attributes.TryGetValue(attributeName, out var attr))
attr.BaseValue = value;
}
}
public class GameplayAttribute
{
public string Name { get; }
public float BaseValue { get; set; }
public float CurrentValue => CalculateCurrentValue();
private List<AttributeModifier> _modifiers = new();
private float CalculateCurrentValue()
{
float value = BaseValue;
// Apply additive modifiers
foreach (var mod in _modifiers.Where(m => m.Operation == ModifierOperation.Add))
value += mod.Value;
// Apply multiplicative modifiers
foreach (var mod in _modifiers.Where(m => m.Operation == ModifierOperation.Multiply))
value *= mod.Value;
// Apply override (last one wins)
var overrides = _modifiers.Where(m => m.Operation == ModifierOperation.Override).ToList();
if (overrides.Any())
value = overrides.Last().Value;
return value;
}
}
public class AttributeModifier
{
public string AttributeName { get; }
public ModifierOperation Operation { get; }
public float Value { get; }
public GameplayEffectHandle SourceEffect { get; }
}
public enum ModifierOperation
{
Add,
Multiply,
Override
}
// Applying modifiers from an effect
private void ApplyModifiers(AbilitySystemComponent target, ActiveGameplayEffect effect)
{
foreach (var modDef in effect.Definition.Modifiers)
{
var modifier = new AttributeModifier
{
AttributeName = modDef.AttributeName,
Operation = modDef.Operation,
Value = CalculateModifierValue(modDef, effect.Context),
SourceEffect = effect.Handle
};
target.AttributeSet.AddModifier(modifier);
}
}
public class GameplayTagContainer
{
private HashSet<GameplayTag> _tags = new();
public bool HasTag(GameplayTag tag)
{
return _tags.Contains(tag) || HasParentTag(tag);
}
public bool HasAny(GameplayTagContainer other)
{
foreach (var tag in other._tags)
{
if (HasTag(tag))
return true;
}
return false;
}
public bool HasAll(GameplayTagContainer other)
{
foreach (var tag in other._tags)
{
if (!HasTag(tag))
return false;
}
return true;
}
public void AddTag(GameplayTag tag)
{
_tags.Add(tag);
}
public void RemoveTag(GameplayTag tag)
{
_tags.Remove(tag);
}
private bool HasParentTag(GameplayTag tag)
{
// Check hierarchical tags (e.g., "Status.Debuff.Stun" matches "Status.Debuff")
var parent = tag.GetParent();
while (parent != null)
{
if (_tags.Contains(parent))
return true;
parent = parent.GetParent();
}
return false;
}
}
// Tag hierarchy example
"Ability.Type.Attack"
"Ability.Type.Attack.Melee"
"Ability.Type.Attack.Ranged"
"Ability.Type.Defense"
"Ability.Type.Defense.Dodge"
"Ability.Type.Defense.Block"
"Status.Buff"
"Status.Buff.Speed"
"Status.Debuff"
"Status.Debuff.Stun"
"Status.Debuff.Slow"
"State.Combat"
"State.Combat.Attacking"
"State.Combat.Blocking"
"State.Movement.Running"
"State.Movement.Jumping"
// BAD: Infinite recursion
public class DamageReflectEffect : GameplayEffectSO
{
// On damage taken, apply damage to attacker
// Attacker has same effect, applies damage back
// INFINITE LOOP!
}
// GOOD: Use recursion guard + source tracking
public GameplayEffectHandle ApplyEffect(...)
{
if (_effectApplicationDepth >= MaxRecursionDepth)
{
Debug.LogError("Recursion limit!");
return GameplayEffectHandle.Invalid;
}
// Also track source to prevent A→B→A chains
if (context.SourceASC == this && effect.CanReflect)
{
return GameplayEffectHandle.Invalid;
}
}
// BAD: Target may be destroyed
public class ProjectileAbility
{
private AbilitySystemComponent _target;
public void OnHit()
{
_target.ApplyEffect(damageEffect); // CRASH if target destroyed
}
}
// GOOD: Null check or weak reference
public void OnHit()
{
if (_target == null || _target.gameObject == null)
return;
_target.ApplyEffect(damageEffect);
}
// BETTER: Use targeting system with validity checks
public void OnHit()
{
if (!_targetHandle.IsValid)
return;
var target = _targetHandle.GetTarget();
target?.ApplyEffect(damageEffect);
}
// BAD: ConcurrentModificationException
foreach (var effect in _activeEffects)
{
if (ShouldRemove(effect))
_activeEffects.Remove(effect); // CRASH
}
// GOOD: Reverse iteration or collect first
for (int i = _activeEffects.Count - 1; i >= 0; i--)
{
if (ShouldRemove(_activeEffects[i]))
RemoveEffectAt(i);
}
// OR
var toRemove = _activeEffects.Where(ShouldRemove).ToList();
foreach (var effect in toRemove)
RemoveEffect(effect);
// BAD: Attribute A depends on B, B depends on A
public float CalculateAttack()
{
return BaseAttack * GetDefenseMultiplier(); // Calls CalculateDefense
}
public float CalculateDefense()
{
return BaseDefense * GetAttackMultiplier(); // Calls CalculateAttack - LOOP!
}
// GOOD: Use dependency order or caching
private bool _isCalculating;
public float CalculateValue()
{
if (_isCalculating)
{
Debug.LogWarning("Circular attribute dependency!");
return BaseValue;
}
_isCalculating = true;
try
{
return DoCalculation();
}
finally
{
_isCalculating = false;
}
}
public void OnCharacterDeath()
{
// Remove all effects
RemoveAllEffects();
// Clear tags
_ownedTags.Clear();
// Cancel active abilities
foreach (var ability in _activeAbilities.ToList())
{
EndAbility(ability);
}
}
public void OnCharacterRespawn()
{
// Reset attributes to base values
foreach (var attr in _attributeSet.Attributes)
{
attr.ClearModifiers();
}
// Apply default effects
foreach (var defaultEffect in _characterDef.DefaultEffects)
{
ApplyEffectToSelf(defaultEffect, new GameplayEffectContext(this));
}
}
public void OnZoneExit()
{
// Remove zone-specific effects
RemoveEffectsWithTag(GameplayTags.Effect_ZoneSpecific);
// Keep persistent effects
// (e.g., equipment, permanent buffs)
}