Skip to content

Instantly share code, notes, and snippets.

@adammyhre
Created September 21, 2025 11:43
Show Gist options
  • Save adammyhre/9efde70222ecdb2254b08bae9ca60ade to your computer and use it in GitHub Desktop.
Save adammyhre/9efde70222ecdb2254b08bae9ca60ade to your computer and use it in GitHub Desktop.
Effects Over Time Example
using System;
using System.Collections.Generic;
using ImprovedTimers; // https://github.com/adammyhre/Unity-Improved-Timers
using UnityEngine;
[Serializable]
public class Ability {
public AudioClip castSfx;
public GameObject castVfx;
public GameObject runningVfx;
[SerializeReference] public List<IEffect<IDamageable>> effects = new();
public void Execute(IDamageable target) {
foreach (var effect in effects) {
if (target is Enemy enemy) {
enemy.ApplyEffect(effect);
} else {
effect.Apply(target);
}
}
}
}
public interface IDamageable {
void TakeDamage(int amount);
}
public interface IEffect<TTarget> {
void Apply(TTarget target);
void Cancel();
event Action<IEffect<TTarget>> OnCompleted;
}
[Serializable]
public class DamageEffect : IEffect<IDamageable> {
public int damageAmount = 10;
public event Action<IEffect<IDamageable>> OnCompleted;
public void Apply(IDamageable target) {
target.TakeDamage(damageAmount);
OnCompleted?.Invoke(this);
}
public void Cancel() {
OnCompleted?.Invoke(this);
}
}
[Serializable]
public class DamageOverTimeEffect : IEffect<IDamageable> {
public float duration = 5f;
public float tickInterval = 1f;
public int damagePerTick;
public event Action<IEffect<IDamageable>> OnCompleted;
IntervalTimer timer;
IDamageable currentTarget;
public void Apply(IDamageable target) {
currentTarget = target;
timer = new IntervalTimer(duration, tickInterval);
timer.OnInterval = OnInterval;
timer.OnTimerStop = OnStop;
timer.Start();
}
void OnInterval() => currentTarget?.TakeDamage(damagePerTick);
void OnStop() => Cleanup();
public void Cancel() {
timer?.Stop();
Cleanup();
}
void Cleanup() {
timer = null;
currentTarget = null;
OnCompleted?.Invoke(this);
}
}
using System.Collections.Generic;
using UnityEngine;
public class Enemy : MonoBehaviour, IDamageable {
public int health = 50;
readonly List<IEffect<IDamageable>> activeEffects = new();
public void TakeDamage(int amount) {
health -= amount;
Debug.Log($"Enemy took {amount} damage. Health now {health}");
if (health <= 0) {
Die();
}
}
public void ApplyEffect(IEffect<IDamageable> effect) {
if (health <= 0) return; // Dead enemies should't receive effects
effect.OnCompleted += RemoveEffect;
activeEffects.Add(effect);
effect.Apply(this);
}
void RemoveEffect(IEffect<IDamageable> effect) {
effect.OnCompleted -= RemoveEffect;
activeEffects.Remove(effect);
}
void Die() {
Debug.Log("Enemy died.");
for (int i = activeEffects.Count - 1; i >= 0; i--) {
var effect = activeEffects[i];
effect.OnCompleted -= RemoveEffect;
effect.Cancel();
}
activeEffects.Clear();
Destroy(gameObject);
}
}
using System.Collections;
using UnityEngine;
using UnityUtils; // https://github.com/adammyhre/Unity-Utils
public class PlayerAbilityCaster : MonoBehaviour {
public Ability[] hotbar;
void Update() {
for (int i = 0; i < hotbar.Length; i++) {
if (Input.GetKeyDown(KeyCode.Alpha1 + i)) {
Cast(hotbar[i], FindFirstObjectByType<Enemy>());
}
}
}
void Cast(Ability ability, IDamageable target) {
ability.Execute(target);
var targetMb = target as MonoBehaviour;
if (ability.castVfx && targetMb) {
Instantiate(ability.castVfx, targetMb.transform.position.Add(y:2), Quaternion.identity);
}
if (ability.runningVfx && targetMb) {
var runningVfxInstance = Instantiate(ability.runningVfx, targetMb.transform);
Destroy(runningVfxInstance, 3f);
}
if (ability.castSfx) {
AudioSource.PlayClipAtPoint(ability.castSfx, transform.position);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment