Last active
October 8, 2025 08:24
-
-
Save adammyhre/3cee767f20a85a8ea67222d3534d9c9e to your computer and use it in GitHub Desktop.
Ability Targeting System
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); | |
| } | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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(); | |
| } | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; | |
| } | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); | |
| } | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); | |
| } | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); | |
| } | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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