Thank you for the replies.
I’ve settled for something that I think will suit my project for the time being.
An AbilityManager that I can pass to any Game Object that cast abilities, such as my player, other players, or various enemies.
I then drag & drop abilities, which are Scriptable Objects instances, to the AbilityManager component. I clone them at runtime to prevent the shared instance thing.
Here is a simplified version of my code (no hotkey, skillbar UI management, …) if it can help others:
public class AbilityManager : MonoBehaviour
{
public Transform abilitySource; // "launcher" of abilities
public Ability[] abilities;
void Awake()
{
Ability ability;
for(int i = 0; i < abilities.Length; i++) {
// clone the object because it's a scriptable objects and we want a unique instance of it.
abilities[i] = Instantiate(abilities[i]);
abilities[i].Initialize(abilitySource);
}
}
void Update()
{
foreach(Ability ability in abilities) {
ability.MakeUpdate();
if(!ability.isOnCooldown()) {
// my custom logic that then Launch the ability based on a hotkey. This is for the player.
// .....
Launch(ability);
}
}
}
// that way an IA component on an enemy can cast an ability too (there is more to that function aswell)
public void Launch(Ability ability) {
ability.Launch();
}
}
And then the Ability base class that I override for my various abilities. It only has a cooldown logic for the sake of simplicity:
public abstract class Ability : ScriptableObject
{
public string displayName = "New Ability";
public float cooldownDuration = 2f;
private float cooldownTimeLeft;
protected Transform spawnPoint;
public void Initialize(Transform source) {
spawnPoint = source;
}
public void Launch() {
if(!isOnCooldown()) {
Trigger();
cooldownTimeLeft = cooldownDuration;
}
}
// Handle the actual code of the ability such as firing a projectile or creating a wall.
protected abstract void Trigger();
public virtual void MakeUpdate() {
if (isOnCooldown()) {
cooldownTimeLeft = cooldownTimeLeft - Time.deltaTime;
}
}
public bool isOnCooldown() {
return cooldownTimeLeft > 0f;
}
public float getCooldownTimeLeft() {
return cooldownTimeLeft;
}
}
And an example of a projectile ability:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[CreateAssetMenu(menuName = "Abilities/Projectile")]
public class ProjectileAbility : Ability
{
public float range;
public float speed;
public Rigidbody projectilePrefab;
protected override void Trigger() {
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hit;
if (Physics.Raycast(ray, out hit, 100))
{
SpawnProjectile(spawnPoint.position, hit.point, range, speed);
}
}
private void SpawnProjectile(Vector3 origin, Vector3 target, float range, float speed)
{
Rigidbody newProjectile = (Rigidbody)Instantiate(projectilePrefab, origin, Quaternion.identity);
ProjectileLifeTime projectileLifeTime = newProjectile.GetComponent<ProjectileLifeTime>();
projectileLifeTime.range = range;
projectileLifeTime.speed = speed;
newProjectile.velocity = (target - spawnPoint.position).normalized * speed;
Collider newProjectileCollider = newProjectile.GetComponent<Collider>();
Physics.IgnoreCollision(newProjectile.GetComponent<Collider>(), spawnPoint.GetComponentInParent<Collider>());
GameObject[] projectiles = GameObject.FindGameObjectsWithTag("Projectile");
foreach (GameObject projectile in projectiles)
{
Physics.IgnoreCollision(newProjectileCollider, projectile.GetComponentInParent<Collider>());
}
}
}
That way I can build multiple abilities and share them easily.
You will note that the targetting is based on mouse position, thus for enemy it doesn’t work well, but that’s my next thing to fix 
Cheers,
Fleb.