Skip to content

Features Effects Effects System Tutorial

github-actions[bot] edited this page Jan 24, 2026 · 7 revisions

Effects System Tutorial - Build Your First Buff in 5 Minutes

What You'll Build

By the end of this tutorial, you'll have a complete working buff system with:

  • A "Haste" buff that increases speed by 50%
  • Visual particle effects that spawn/despawn with the buff
  • A "Stunned" debuff that prevents player movement
  • Tags you can query in gameplay code

Time required: 5-10 minutes


Why Use the Effects System?

The Old Way:

// 50-100 lines per effect type
public class HasteEffect : MonoBehaviour {
    float duration;
    float speedMultiplier;
    GameObject particles;

    void Update() {
        duration -= Time.deltaTime;
        if (duration <= 0) RemoveSelf();
    }

    void RemoveSelf() {
        // Remove speed modifier...
        // Destroy particles...
        // Handle stacking...
        // 40 more lines...
    }
}

The New Way:

// Zero lines - everything configured in editor
player.ApplyEffect(hasteEffect);  // Done!

Result: Designers create hundreds of effects without programmer involvement.


Step 1: Create Your First AttributesComponent (2 minutes)

This component will hold the stats that effects can modify.

using UnityEngine;
using WallstopStudios.UnityHelpers.Tags;

public class PlayerStats : AttributesComponent
{
    // Define attributes that effects can modify
    public Attribute Speed = 5f;
    public Attribute MaxHealth = 100f;
    public Attribute AttackDamage = 10f;
    public Attribute Defense = 5f;

    protected override void Awake()
    {
        base.Awake();
        // Optional: Log when attributes change
        OnAttributeModified += (attributeName, oldVal, newVal) =>
            Debug.Log($"{attributeName} changed: {oldVal}{newVal}");
    }
}

What's an Attribute?

  • Holds a base value (e.g., Speed = 5)
  • Tracks modifications from multiple sources
  • Calculates final value automatically (Add → Multiply → Override)
  • Raises events when value changes

⚠️ Important: Use Attributes for "max" or "rate" values, NOT "current" depleting values!

  • MaxHealth - modified by buffs (good)
  • CurrentHealth - modified by damage/healing from many systems (bad - causes state conflicts)
  • AttackDamage - modified by strength buffs (good)
  • Speed - modified by haste/slow effects (good)

If a value is frequently modified by systems outside the effects system (like health being reduced by damage), use a regular field instead. See the main documentation for details.


Step 2: Add Stats to Your Player (30 seconds)

  1. Open your Player prefab/GameObject
  2. Add Component → PlayerStats
  3. Set values in Inspector:
    • Speed: 5
    • MaxHealth: 100
    • AttackDamage: 10
    • Defense: 5

That's it! Your player now has modifiable attributes.


Step 3: Create a Haste Effect (2 minutes)

3.1 Create the ScriptableObject

  1. In Project window: Right-clickCreateWallstop StudiosUnity HelpersAttribute Effect
  2. Name it: HasteEffect

3.2 Configure the Effect

Select HasteEffect and set these values in Inspector:

Modifications:

  • Click "+" to add a modification
  • Attribute Name: Speed (must match field name exactly)
  • Action: Multiplication
  • Value: 1.5 (150% of base speed)

Duration:

  • Modifier Duration Type: Duration
  • Duration: 5 (seconds)
  • Can Reapply: ✅ (checking this resets timer when reapplied)

Tags:

  • Effect Tags: Add "Haste" (used for both gameplay queries via HasTag() and effect organization)

3.3 Add Visual Effects (Optional)

Cosmetic Effects:

  • Size: 1
  • Element 0:
    • Prefab: Drag a particle system prefab (or create one)
    • Requires Instancing: ✅ (creates a new instance per application)

Step 4: Apply the Effect (30 seconds)

Add this code to test your effect:

using UnityEngine;
using WallstopStudios.UnityHelpers.Tags;

public class PlayerController : MonoBehaviour
{
    [SerializeField] private AttributeEffect hasteEffect;
    private PlayerStats stats;

    void Start()
    {
        stats = GetComponent<PlayerStats>();
    }

    void Update()
    {
        // Apply haste when pressing H
        if (Input.GetKeyDown(KeyCode.H))
        {
            this.ApplyEffect(hasteEffect);
            Debug.Log($"Speed is now: {stats.Speed.CurrentValue}");
        }

        // Move with current speed
        float h = Input.GetAxis("Horizontal");
        transform.position += Vector3.right * h * stats.Speed * Time.deltaTime;
    }
}

Test it:

  1. Assign HasteEffect to the Inspector field
  2. Press Play
  3. Press H to apply haste
  4. Notice: Speed increases to 7.5, particle effect spawns
  5. After 5 seconds: Speed returns to 5, particles disappear

Step 5: Create a Stun Debuff (2 minutes)

Let's make a more complex effect that prevents movement.

5.1 Create the Effect

  1. Right-clickCreateWallstop StudiosUnity HelpersAttribute Effect
  2. Name it: StunEffect

5.2 Configure Stun

Modifications:

  • Attribute Name: Speed
  • Action: Override
  • Value: 0 (completely override speed to 0)

Duration:

  • Modifier Duration Type: Duration
  • Duration: 3
  • Can Reapply: ✅

Tags:

  • Effect Tags: "Stunned", "Stun", "Debuff", "CC" (for gameplay queries and organization)

5.3 Query Tags in Gameplay

public class PlayerController : MonoBehaviour
{
    [SerializeField] private AttributeEffect hasteEffect;
    [SerializeField] private AttributeEffect stunEffect;
    private PlayerStats stats;

    void Update()
    {
        // Apply effects
        if (Input.GetKeyDown(KeyCode.H)) this.ApplyEffect(hasteEffect);
        if (Input.GetKeyDown(KeyCode.S)) this.ApplyEffect(stunEffect);

        // Check if player is stunned before allowing movement
        if (this.HasTag("Stunned"))
        {
            Debug.Log("Player is stunned! Cannot move.");
            return;
        }

        // Normal movement
        float h = Input.GetAxis("Horizontal");
        transform.position += Vector3.right * h * stats.Speed * Time.deltaTime;
    }
}

Test it:

  1. Press S to stun yourself
  2. Try to move - you can't!
  3. After 3 seconds, movement returns

Step 6: Advanced Features (5 minutes)

Stacking Effects

Effects stack independently by default:

// Apply haste 3 times
this.ApplyEffect(hasteEffect);  // Speed = 7.5
this.ApplyEffect(hasteEffect);  // Speed = 11.25 (1.5 × 1.5 × 5)
this.ApplyEffect(hasteEffect);  // Speed = 16.875 (1.5 × 1.5 × 1.5 × 5)

// Each stack has its own duration and can be removed independently

Manual Removal

// Apply and save handle
EffectHandle? handle = this.ApplyEffect(hasteEffect);

// Remove specific stack early
if (handle.HasValue)
{
    this.RemoveEffect(handle.Value);
}

// Remove all haste effects
this.RemoveEffects(this.GetHandlesWithTag("Haste"));

Multiple Modifications Per Effect

One effect can modify multiple attributes:

Create "Berserker Rage" effect:

  • Modification 1: Speed × 1.3
  • Modification 2: AttackDamage × 2.0
  • Modification 3: Defense × 0.5 (trade-off - more damage but less defense!)
  • Duration: 10 seconds
  • Tags: "Berserker", "Buff"

Infinite Duration Effects

For permanent buffs (e.g., equipment):

// In Inspector:
// - Modifier Duration Type: Infinite
// - (Duration field is ignored)

// Apply permanent buff
EffectHandle? handle = this.ApplyEffect(permanentStrengthBonus);

// Later, remove when equipment is unequipped
if (handle.HasValue)
    this.RemoveEffect(handle.Value);

Common Patterns

Damage Over Time (DOT)

// Create "Poison" effect:
// - periodicEffects: interval = 1s, maxTicks = 10, modifications = []
// - behaviors: PoisonDamageBehavior (below)
// - Duration: 10 seconds
// - Tags: "Poisoned", "DoT", "Debuff"

void ApplyPoison(GameObject target)
{
    target.ApplyEffect(poisonEffect);
}

[CreateAssetMenu(menuName = "Combat/Effects/Poison Damage")]
public sealed class PoisonDamageBehavior : EffectBehavior
{
    [SerializeField]
    private float damagePerTick = 2f;

    public override void OnPeriodicTick(
        EffectBehaviorContext context,
        PeriodicEffectTickContext tickContext
    )
    {
        if (!context.Target.TryGetComponent(out PlayerHealth health))
        {
            return;
        }

        health.ApplyDamage(damagePerTick);
    }
}

public sealed class PlayerHealth : MonoBehaviour
{
    [SerializeField]
    private float currentHealth = 100f;

    public float CurrentHealth => currentHealth;

    public void ApplyDamage(float amount)
    {
        currentHealth -= amount;

        if (currentHealth <= 0f)
        {
            currentHealth = 0f;
            Die();
        }
    }

    private void Die()
    {
        // Handle player death
    }
}

This keeps CurrentHealth as a regular gameplay field while the effect system triggers damage through behaviours.

Cooldown Reduction

// Create "Haste" effect (for abilities):
// - Modification: CooldownRate × 1.5 (50% faster cooldowns)

public class AbilitySystem : AttributesComponent
{
    public Attribute CooldownRate = 1f;
    private float cooldown;

    public void UseAbility()
    {
        // Cooldown respects rate
        cooldown = baseCooldown / CooldownRate.Value;
    }
}

Conditional Effects

// Only apply effect if conditions met
void TryApplyBuff(AttributeEffect effect)
{
    // Check if player already has max buffs
    if (this.TryGetTagCount("Buff", out int buffCount) && buffCount >= 5)
    {
        Debug.Log("Too many buffs active!");
        return;
    }

    // Check if effect is already active
    if (this.HasTag("Haste") && effect == hasteEffect)
    {
        Debug.Log("Haste already active!");
        return;
    }

    this.ApplyEffect(effect);
}

Troubleshooting

"Should I use CurrentHealth as an Attribute?"

  • No! Use MaxHealth as an Attribute (modified by buffs), but keep CurrentHealth as a regular field (modified by damage/healing)
  • Why: CurrentHealth is modified by many systems (combat, regeneration, etc.). Using it as an Attribute causes state conflicts when effects and other systems both try to modify it
  • Pattern: Attribute for max/cap, regular field for current/depleting value
  • See: "Understanding Attributes: What to Model and What to Avoid" in the main documentation

"Attribute 'Speed' not found"

  • Cause: Attribute name in effect doesn't match the field name in AttributesComponent
  • Fix: Names must match exactly (case-sensitive): Speed not speed
  • Tip: Use Attribute Metadata Cache generator for dropdown validation

Effect doesn't apply

  • Check: Does target GameObject have an AttributesComponent?
  • Check: Is EffectHandler component added? (Usually added automatically)
  • Check: Are there any errors in the console?

Particles don't spawn

  • Check: Cosmetic Effects → Prefab is assigned
  • Check: Prefab has a CosmeticEffectData component
  • Check: Requires Instancing is checked if using per-application instances

Value isn't changing

  • Check: Attribute name matches exactly
  • Check: Modification value is non-zero
  • Check: Action type is correct (Multiplication needs > 0, Addition can be negative)
  • Debug: Log attribute.Value before and after applying effect

Next Steps

You now have a complete buff/debuff system! Here are some ideas to expand:

Create More Effects

  • Shield: MaxHealth × 1.5, visual shield sprite
  • Slow: Speed × 0.5, "Slowed" tag
  • Critical Strike: AttackDamage × 2.0, "CriticalHit" tag, brief flash effect
  • Invisibility: Just tags ("Invisible"), no stat changes, transparency effect
  • Armor Buff: Defense + 10, metallic sheen cosmetic
  • Strength Potion: AttackDamage × 1.5, red particle aura

Build Systems Around Tags

// AI ignores invisible players
if (!target.HasTag("Invisible"))
{
    ChasePlayer(target);
}

// UI shows status icons
if (player.HasTag("Poisoned"))
    ShowPoisonIcon();

// Abilities check prerequisites
if (player.HasTag("Stunned") || player.HasTag("Silenced"))
    return;  // Can't cast

// Interactions respect state
if (player.HasTag("Invulnerable"))
    damage = 0;

Designer Workflows

  1. Create an effect library (30+ common effects)
  2. Designers mix/match on items, abilities, enemies
  3. Programmers never touch effect code again!

📚 Related Documentation

Core Guides:

Related Features:

Need help? Open an issue


Made with ❤️ by Wallstop Studios

Effects System tutorial complete! Your designers can now create gameplay effects without code.

Clone this wiki locally