Skip to content

Instantly share code, notes, and snippets.

@adammyhre
Created May 25, 2025 10:34
Show Gist options
  • Save adammyhre/8e6f57d24e96d33738273ed0570828be to your computer and use it in GitHub Desktop.
Save adammyhre/8e6f57d24e96d33738273ed0570828be to your computer and use it in GitHub Desktop.
Modular Ability Effects
using System;
using System.Collections.Generic;
using UnityEngine;
[CreateAssetMenu(fileName = "AbilityData", menuName = "ScriptableObjects/AbilityData")]
class AbilityData : ScriptableObject {
public string label;
public AnimationClip animationClip;
[Range(0.1f, 4f)] public float castTime = 2f;
public ProjectileMove vfxPrefab;
[SerializeReference] public List<AbilityEffect> effects;
void OnEnable() {
if (string.IsNullOrEmpty(label)) label = name;
if (effects == null) effects = new List<AbilityEffect>();
}
}
[Serializable]
abstract class AbilityEffect {
public abstract void Execute(GameObject caster, GameObject target);
}
[Serializable]
class DamageEffect : AbilityEffect {
public int amount;
public override void Execute(GameObject caster, GameObject target) {
target.GetComponent<Health>().ApplyDamage(amount);
Debug.Log($"{caster.name} dealt {amount} damage to {target.name}");
}
}
[Serializable]
class KnockbackEffect : AbilityEffect {
public float force;
public override void Execute(GameObject caster, GameObject target) {
var dir = (target.transform.position - caster.transform.position).normalized;
target.GetComponent<Rigidbody>().AddForce(dir * force, ForceMode.Impulse);
Debug.Log($"{caster.name} knocked back {target.name} with force {force}");
}
}
using UnityEngine;
using UnityEditor;
using System;
using System.Linq;
using System.Collections.Generic;
[CustomPropertyDrawer(typeof(AbilityEffect), true)]
public class AbilityEffectDrawer : PropertyDrawer {
static Dictionary<string, Type> typeMap;
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) {
if (typeMap == null) BuildTypeMap();
var typeRect = new Rect(position.x, position.y, position.width, EditorGUIUtility.singleLineHeight);
var contentRect = new Rect(position.x, position.y + EditorGUIUtility.singleLineHeight, position.width, position.height - EditorGUIUtility.singleLineHeight);
EditorGUI.BeginProperty(position, label, property);
var typeName = property.managedReferenceFullTypename;
var displayName = GetShortTypeName(typeName);
if (EditorGUI.DropdownButton(typeRect, new GUIContent(displayName ?? "Select Effect Type"), FocusType.Keyboard)) {
var menu = new GenericMenu();
if (typeMap == null || typeMap.Count == 0) {
menu.AddDisabledItem(new GUIContent("No Ability Effects available"));
menu.ShowAsContext();
return;
}
foreach (var kvp in typeMap) {
var name = kvp.Key;
var type = kvp.Value;
menu.AddItem(new GUIContent(name), type.FullName == typeName, () => {
property.managedReferenceValue = Activator.CreateInstance(type);
property.serializedObject.ApplyModifiedProperties();
});
}
menu.ShowAsContext();
}
if (property.managedReferenceValue != null) {
EditorGUI.indentLevel++;
EditorGUI.PropertyField(contentRect, property, GUIContent.none, true);
EditorGUI.indentLevel--;
}
EditorGUI.EndProperty();
}
public override float GetPropertyHeight(SerializedProperty property, GUIContent label) {
return EditorGUI.GetPropertyHeight(property, label, true) + EditorGUIUtility.singleLineHeight;
}
static void BuildTypeMap() {
var baseType = typeof(AbilityEffect);
typeMap = AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(asm => {
try { return asm.GetTypes(); }
catch { return Type.EmptyTypes; }
})
.Where(t => !t.IsAbstract && baseType.IsAssignableFrom(t))
.ToDictionary(t => ObjectNames.NicifyVariableName(t.Name), t => t);
}
static string GetShortTypeName(string fullTypeName) {
if (string.IsNullOrEmpty(fullTypeName)) return null;
var parts = fullTypeName.Split(' ');
return parts.Length > 1 ? parts[1].Split('.').Last() : fullTypeName;
}
}
using System.Collections;
using UnityEngine;
using UnityEngine.InputSystem;
using AdvancedController;
using ImprovedTimers;
class AbilityExecutor : MonoBehaviour {
[SerializeField] AbilityData ability;
[SerializeField] GameObject target;
AnimationController animationController;
CountdownTimer castTimer;
void Awake() {
animationController = GetComponent<AnimationController>();
castTimer = new CountdownTimer(ability.castTime);
castTimer.OnTimerStart = () => animationController.OrNull()?.PlayOneShot(ability.animationClip);
castTimer.OnTimerStop = () => SpawnVFX();
}
void SpawnVFX() {
if (ability.vfxPrefab == null) return;
var vfx = Instantiate(ability.vfxPrefab, transform.position, transform.rotation);
vfx.SetCallback((Collision co) => {
foreach (var effect in ability.effects) {
effect.Execute(gameObject, target);
}
});
Destroy(vfx, 5f);
}
public void Execute(GameObject target) {
castTimer.Start();
}
void Update() {
if (Keyboard.current.spaceKey.wasPressedThisFrame) {
Execute(target);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment