Skip to content

Instantly share code, notes, and snippets.

@adammyhre
Last active October 8, 2025 08:24
Show Gist options
  • Save adammyhre/3cee767f20a85a8ea67222d3534d9c9e to your computer and use it in GitHub Desktop.
Save adammyhre/3cee767f20a85a8ea67222d3534d9c9e to your computer and use it in GitHub Desktop.
Ability Targeting System
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityUtils;
using Object = UnityEngine.Object;
[Serializable]
public class Ability {
public AudioClip castSfx;
public GameObject castVfx;
public GameObject runningVfx;
[Header("Effects")]
[SerializeReference] public List<IEffectFactory<IDamageable>> effects = new();
[Header("Targeting")]
[SerializeReference] TargetingStrategy targetingStrategy;
public void Target(TargetingManager targetingManager) {
if (targetingStrategy != null) {
targetingStrategy.Start(this, targetingManager);
}
}
public void Execute(IDamageable target) {
HandleVFX(target);
foreach (var effect in effects) {
var runtimeEffect = effect.Create();
target.ApplyEffect(runtimeEffect);
}
}
void HandleVFX(IDamageable target) {
var targetMb = target as MonoBehaviour;
if (targetMb == null) return;
if (castVfx != null) {
Object.Instantiate(castVfx, targetMb.transform.position.Add(y:2), Quaternion.identity);
}
if (runningVfx != null) {
var runningVfxInstance = Object.Instantiate(runningVfx, targetMb.transform);
Object.Destroy(runningVfxInstance, 3f);
}
}
}
using System;
using System.Linq;
using Unity.Cinemachine;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityUtils;
[Serializable]
public class AOETargeting : TargetingStrategy {
public GameObject aoePrefab;
public float aoeRadius = 5f;
public LayerMask groundLayerMask = 1;
GameObject previewInstance;
public override void Start(Ability ability, TargetingManager targetingManager) {
this.ability = ability;
this.targetingManager = targetingManager;
Cancel();
isTargeting = true;
ToggleCameraOrbit(false);
targetingManager.SetCurrentStrategy(this);
if (aoePrefab != null) {
previewInstance = UnityEngine.Object.Instantiate(aoePrefab, Vector3.zero.Add(y:0.1f), Quaternion.identity);
}
if (targetingManager.input != null) {
targetingManager.input.Click += OnClick;
}
}
public override void Update() {
if (!isTargeting || previewInstance == null) return;
previewInstance.transform.position = GetMouseWorldPosition().Add(y: 0.1f);
}
Vector3 GetMouseWorldPosition() {
if (targetingManager.cam == null) return Vector3.zero;
var ray = targetingManager.cam.ScreenPointToRay(Mouse.current.position.ReadValue());
return Physics.Raycast(ray, out var hit, 100f, groundLayerMask) ? hit.point : Vector3.zero;
}
void ToggleCameraOrbit(bool enabled) {
var inputAxisControllers = targetingManager.GetComponentInChildren<CinemachineInputAxisController>();
if (inputAxisControllers != null) {
inputAxisControllers.enabled = enabled;
}
}
public override void Cancel() {
isTargeting = false;
ToggleCameraOrbit(true);
targetingManager.ClearCurrentStrategy();
if (previewInstance != null) {
UnityEngine.Object.Destroy(previewInstance);
}
if (targetingManager.input != null) {
targetingManager.input.Click -= OnClick;
}
}
void OnClick(RaycastHit hit) {
if (isTargeting) {
var targets = Physics.OverlapSphere(hit.point, aoeRadius)
.Select(c => c.GetComponent<IDamageable>())
.OfType<IDamageable>();
foreach (var target in targets) {
ability.Execute(target);
}
Cancel();
}
}
}
using System;
using ImprovedTimers;
public interface IDamageable {
void TakeDamage(int amount);
void ApplyEffect(IEffect<IDamageable> effect);
}
public interface IEffectFactory<TTarget> {
IEffect<TTarget> Create();
}
public interface IEffect<TTarget> {
void Apply(TTarget target);
void Cancel();
event Action<IEffect<TTarget>> OnCompleted;
}
[Serializable]
public class DamageEffectFactory : IEffectFactory<IDamageable> {
public int damageAmount = 10;
public IEffect<IDamageable> Create() {
return new DamageEffect { damageAmount = damageAmount };
}
}
[Serializable]
public struct DamageEffect : IEffect<IDamageable> {
public int damageAmount;
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 DamageOverTimeEffectFactory : IEffectFactory<IDamageable> {
public float duration = 3f;
public float tickInterval = 1f;
public int damagePerTick = 5;
public IEffect<IDamageable> Create() {
return new DamageOverTimeEffect {
duration = duration,
tickInterval = tickInterval,
damagePerTick = damagePerTick
};
}
}
[Serializable]
public struct DamageOverTimeEffect : IEffect<IDamageable> {
public float duration;
public float tickInterval;
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 UnityEngine;
using UnityEngine.Events;
using UnityEngine.InputSystem;
using static PlayerInputActions;
public interface IInputReader {
Vector2 Direction { get; }
void EnablePlayerActions();
}
[CreateAssetMenu(fileName = "InputReader", menuName = "InputReader")]
public class InputReader : ScriptableObject, IPlayerActions, IInputReader {
public event UnityAction<Vector2> Move = delegate { };
public event UnityAction<Vector2, bool> Look = delegate { };
public event UnityAction EnableMouseControlCamera = delegate { };
public event UnityAction DisableMouseControlCamera = delegate { };
public event UnityAction<bool> Jump = delegate { };
public event UnityAction<bool> Dash = delegate { };
public event UnityAction Attack = delegate { };
public event UnityAction<RaycastHit> Click = delegate { };
public PlayerInputActions inputActions;
public bool IsJumpKeyPressed() => inputActions.Player.Jump.IsPressed();
public Vector2 Direction => inputActions.Player.Move.ReadValue<Vector2>();
public void EnablePlayerActions() {
if (inputActions == null) {
inputActions = new PlayerInputActions();
inputActions.Player.SetCallbacks(this);
}
inputActions.Enable();
}
public void OnMove(InputAction.CallbackContext context) {
Move.Invoke(context.ReadValue<Vector2>());
}
public void OnLook(InputAction.CallbackContext context) {
Look.Invoke(context.ReadValue<Vector2>(), IsDeviceMouse(context));
}
bool IsDeviceMouse(InputAction.CallbackContext context) {
// Debug.Log($"Device name: {context.control.device.name}");
return context.control.device.name == "Mouse";
}
public void OnFire(InputAction.CallbackContext context) {
if (context.phase == InputActionPhase.Started) {
if (IsDeviceMouse(context)) {
var ray = Camera.main.ScreenPointToRay(Mouse.current.position.ReadValue());
if (Physics.Raycast(ray.origin, ray.direction, out var hit, 100)) {
Click.Invoke(hit);
}
}
}
}
public void OnMouseControlCamera(InputAction.CallbackContext context) {
switch (context.phase) {
case InputActionPhase.Started:
EnableMouseControlCamera.Invoke();
break;
case InputActionPhase.Canceled:
DisableMouseControlCamera.Invoke();
break;
}
}
public void OnRun(InputAction.CallbackContext context) {
switch (context.phase) {
case InputActionPhase.Started:
Dash.Invoke(true);
break;
case InputActionPhase.Canceled:
Dash.Invoke(false);
break;
}
}
public void OnJump(InputAction.CallbackContext context) {
switch (context.phase) {
case InputActionPhase.Started:
Jump.Invoke(true);
break;
case InputActionPhase.Canceled:
Jump.Invoke(false);
break;
}
}
}
using UnityEngine;
[RequireComponent(typeof(TargetingManager))]
public class PlayerAbilityCaster : MonoBehaviour {
public Ability[] hotbar;
public TargetingManager targetingManager;
void Update() {
for (int i = 0; i < hotbar.Length; i++) {
if (Input.GetKeyDown(KeyCode.Alpha1 + i)) {
Cast(hotbar[i]);
}
}
}
void Cast(Ability ability) {
ability.Target(targetingManager);
if (ability.castSfx) {
AudioSource.PlayClipAtPoint(ability.castSfx, transform.position);
}
}
}
using UnityEngine;
public class ProjectileController : MonoBehaviour {
Ability ability;
float speed;
public void Initialize(Ability ability, float speed) {
this.ability = ability;
this.speed = speed;
Destroy(gameObject, 5f);
}
void Update() => transform.Translate(Vector3.forward * (speed * Time.deltaTime));
void OnTriggerEnter(Collider other) {
if (other.CompareTag("Player")) return;
if (other.gameObject.TryGetComponent<IDamageable>(out var target)) {
ability.Execute(target);
Destroy(gameObject);
}
}
}
using UnityEngine;
using UnityUtils;
public class ProjectileTargeting : TargetingStrategy {
public GameObject projectilePrefab;
public float projectileSpeed = 10f;
public override void Start(Ability ability, TargetingManager targetingManager) {
this.ability = ability;
this.targetingManager = targetingManager;
if (projectilePrefab != null) {
var flatForward = targetingManager.cam.transform.forward.With(y:0).normalized;
var forwardRotation = Quaternion.LookRotation(flatForward);
var projectile = Object.Instantiate(projectilePrefab, targetingManager.transform.position.Add(y:1), forwardRotation);
projectile.GetComponent<ProjectileController>().Initialize(ability, projectileSpeed);
}
}
}
using UnityEngine;
public class TargetingManager : MonoBehaviour {
public InputReader input;
public Camera cam;
TargetingStrategy currentStrategy;
void Update() {
if (currentStrategy != null && currentStrategy.IsTargeting) {
currentStrategy.Update();
}
}
public void SetCurrentStrategy(TargetingStrategy strategy) => currentStrategy = strategy;
public void ClearCurrentStrategy() => currentStrategy = null;
}
public abstract class TargetingStrategy {
protected Ability ability;
protected TargetingManager targetingManager;
protected bool isTargeting = false;
public bool IsTargeting => isTargeting;
public abstract void Start(Ability ability, TargetingManager targetingManager);
public virtual void Update() { }
public virtual void Cancel() { }
}
public class SelfTargeting : TargetingStrategy {
public override void Start(Ability ability, TargetingManager targetingManager) {
if (targetingManager.transform.TryGetComponent<IDamageable>(out var target)) {
ability.Execute(target);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment