본문으로 건너뛰기
cccskills
무료GitHub에 공개

tools-unity-gameplay-ability-system

Gameplay Ability System patterns for abilities, effects, attributes, and tags including recursion safety and lifecycle management.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

설치 방법 보기

포함된 파일(1)

  • SKILL.md17.0 KB

SKILL.md(원문)

설치하기 전에 에이전트에게 전달되는 지시 내용을 확인할 수 있습니다.

Gameplay Ability System (GAS)

Overview

The Gameplay Ability System provides a framework for abilities, effects, attributes, and gameplay tags. This skill covers safe implementation patterns and common pitfalls.

When to Use

  • Implementing combat abilities
  • Managing gameplay effects (buffs, debuffs, damage)
  • Attribute-based character stats
  • Tag-based gameplay queries
  • Status effect systems

Core Components

Component Hierarchy

AbilitySystemComponent (ASC)
├── Granted Abilities[]
├── Active Effects[]
├── Attribute Sets[]
└── Owned Tags[]

AbilitySystemComponent

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;
}

Abilities

Ability Specification

[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);
    }
}

Ability Activation Flow

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);
    }
}

Gameplay Effects

Effect Definition

[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
}

Effect Application (Safe Pattern)

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);
        }
    }
}

Effect Removal

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);
        }
    }
}

Effect Lifecycle Update

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;
                }
            }
        }
    }
}

Attributes

Attribute Definition

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;
    }
}

Attribute Modification

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);
    }
}

Gameplay Tags

Tag Container

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;
    }
}

Common Tag Patterns

// 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"

Common Pitfalls & Solutions

Pitfall 1: Effect Recursion

// 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;
    }
}

Pitfall 2: Null Target References

// 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);
}

Pitfall 3: Modifying Collection During Iteration

// 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);

Pitfall 4: Stack Overflow in Attribute Calculation

// 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;
    }
}

Cleanup Patterns

Effect Cleanup on Death/Respawn

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));
    }
}

Scene/Zone Transition Cleanup

public void OnZoneExit()
{
    // Remove zone-specific effects
    RemoveEffectsWithTag(GameplayTags.Effect_ZoneSpecific);
    
    // Keep persistent effects
    // (e.g., equipment, permanent buffs)
}

Best Practices

  1. Always guard against recursion in effect application
  2. Use handles instead of direct references for targets
  3. Iterate safely when modifying collections
  4. Pool effect instances for frequently applied effects
  5. Use tags extensively for queries instead of type checks
  6. Clear effects properly during state transitions
  7. Log effect chains for debugging
  8. Set reasonable stack limits to prevent infinite stacking
  9. Test edge cases like self-targeting, simultaneous effects

리뷰

아직 리뷰가 없습니다. 사용해 본 소감을 남겨 주세요.

댓글

로그인 없이 쓸 수 있습니다. 사용 후기, 질문, 작동하지 않은 환경 등을 편하게 남겨 주세요.

아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.

보내면 바로 공개됩니다. 이메일 주소나 전화번호 같은 개인 정보는 쓰지 마세요. URL은 2개까지입니다.

0 / 1,000자

이 양식은 reCAPTCHA로 보호되며 Google 개인정보처리방침 및 서비스 약관이 적용됩니다.

같은 저장소의 스킬

adopt

무료

Brownfield onboarding — audits existing project artifacts for template format compliance (not just existence), classifies gaps by impact, and produces a numbered migration plan. Run this when joining an in-progress project or upgrading from an older template version. Distinct from /project-stage-detect (which checks what exists) — this checks whether what exists will actually work with the template's skills.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

IdoCohen560/claude-unity-game-studio222026년 7월 8일 업데이트

Creates an Architecture Decision Record (ADR) documenting a significant technical decision, its context, alternatives considered, and consequences. Every major technical choice should have an ADR.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

IdoCohen560/claude-unity-game-studio222026년 7월 8일 업데이트

Validates completeness and consistency of the project architecture against all GDDs. Builds a traceability matrix mapping every GDD technical requirement to ADRs, identifies coverage gaps, detects cross-ADR conflicts, verifies engine compatibility consistency across all decisions, and produces a PASS/CONCERNS/FAIL verdict. The architecture equivalent of /design-review.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

IdoCohen560/claude-unity-game-studio222026년 7월 8일 업데이트

art-bible

무료

Guided, section-by-section Art Bible authoring. Creates the visual identity specification that gates all asset production. Run after /brainstorm is approved and before /map-systems or any GDD authoring begins.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

IdoCohen560/claude-unity-game-studio222026년 7월 8일 업데이트

Audits game assets for compliance with naming conventions, file size budgets, format standards, and pipeline requirements. Identifies orphaned assets, missing references, and standard violations.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

IdoCohen560/claude-unity-game-studio222026년 7월 8일 업데이트

Generate per-asset visual specifications and AI generation prompts from GDDs, level docs, or character profiles. Produces structured spec files and updates the master asset manifest. Run after art bible and GDD/level design are approved, before production begins.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

IdoCohen560/claude-unity-game-studio222026년 7월 8일 업데이트

IdoCohen560의 스킬 모두 보기

이 스킬의 문제 신고