com.wallstop-studios.unity-helpers
Version:
Treasure chest of Unity developer tools
526 lines (378 loc) • 13.4 kB
Markdown
# 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:**
```csharp
// 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:**
```csharp
// 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.
```csharp
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-click` → `Create` → `Wallstop Studios` → `Unity Helpers` → `Attribute 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:
```csharp
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-click` → `Create` → `Wallstop Studios` → `Unity Helpers` → `Attribute 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
```csharp
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:
```csharp
// 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
```csharp
// 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):
```csharp
// 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)
```csharp
// 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
```csharp
// 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
```csharp
// 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
```csharp
// 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:**
- [Effects System Full Guide](./effects-system.md) - Complete API reference and advanced patterns
- [Getting Started](../../overview/getting-started.md) - Your first 5 minutes with Unity Helpers
- [Main README](../../readme.md) - Complete feature overview
**Related Features:**
- [Relational Components](../relational-components/relational-components.md) - Auto-wire components (pairs well with effects)
- [Serialization](../serialization/serialization.md) - Save/load effects and attributes
**Need help?** [Open an issue](https://github.com/wallstop/unity-helpers/issues)
---
### Made with ❤️ by Wallstop Studios
_Effects System tutorial complete! Your designers can now create gameplay effects without code._