-
Notifications
You must be signed in to change notification settings - Fork 7
Features Effects Effects System Tutorial
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
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.
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
- ✅ 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.
- Open your Player prefab/GameObject
- Add Component →
PlayerStats - Set values in Inspector:
- Speed:
5 - MaxHealth:
100 - AttackDamage:
10 - Defense:
5
- Speed:
That's it! Your player now has modifiable attributes.
- In Project window:
Right-click→Create→Wallstop Studios→Unity Helpers→Attribute Effect - Name it:
HasteEffect
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 viaHasTag()and effect organization)
Cosmetic Effects:
- Size:
1 - Element 0:
- Prefab: Drag a particle system prefab (or create one)
- Requires Instancing: ✅ (creates a new instance per application)
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:
- Assign
HasteEffectto the Inspector field - Press Play
- Press
Hto apply haste - Notice: Speed increases to 7.5, particle effect spawns
- After 5 seconds: Speed returns to 5, particles disappear
Let's make a more complex effect that prevents movement.
-
Right-click→Create→Wallstop Studios→Unity Helpers→Attribute Effect - Name it:
StunEffect
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)
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:
- Press
Sto stun yourself - Try to move - you can't!
- After 3 seconds, movement returns
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// 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"));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"
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);// 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.
// 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;
}
}// 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);
}-
No! Use
MaxHealthas an Attribute (modified by buffs), but keepCurrentHealthas 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
- Cause: Attribute name in effect doesn't match the field name in AttributesComponent
-
Fix: Names must match exactly (case-sensitive):
Speednotspeed - Tip: Use Attribute Metadata Cache generator for dropdown validation
-
Check: Does target GameObject have an
AttributesComponent? -
Check: Is
EffectHandlercomponent added? (Usually added automatically) - Check: Are there any errors in the console?
- Check: Cosmetic Effects → Prefab is assigned
-
Check: Prefab has a
CosmeticEffectDatacomponent - Check: Requires Instancing is checked if using per-application instances
- 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.Valuebefore and after applying effect
You now have a complete buff/debuff system! Here are some ideas to expand:
- 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
// 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;- Create an effect library (30+ common effects)
- Designers mix/match on items, abilities, enemies
- Programmers never touch effect code again!
Core Guides:
- Effects System Full Guide - Complete API reference and advanced patterns
- Getting Started - Your first 5 minutes with Unity Helpers
- Main README - Complete feature overview
Related Features:
- Relational Components - Auto-wire components (pairs well with effects)
- Serialization - Save/load effects and attributes
Need help? Open an issue
Effects System tutorial complete! Your designers can now create gameplay effects without code.
📦 Unity Helpers | 📖 Documentation | 🐛 Issues | 📜 MIT License
- Inspector Button
- Inspector Conditional Display
- Inspector Grouping Attributes
- Inspector Inline Editor
- Inspector Overview
- Inspector Selection Attributes
- Inspector Settings
- Inspector Validation Attributes
- Utility Components
- Visual Components
- Data Structures
- Helper Utilities
- Math And Extensions
- Pooling Guide
- Random Generators
- Reflection Helpers
- Singletons