Skill casting method and secondary effects

Hi, first of all I’m probably aiming way too far for a game idea. I did successfully make what I want, but I can’t help but always think I’m doing things wrong. The main intention of posting this here is to let others comment on how I designed the whole system.
Prototype is here: http://dl.dropboxusercontent.com/u/81093784/GotGWeb.html

The game I’m making is a fully 3d shooter. It’s online, and I’ll have it read/write to external file about the player’s character like stats, skills, items(not in the game yet), etc.

Basically there are several ways to cast spells. Shot, Projectile, Cursor, Target, Self.
Shot is typical gun shot ish, sending raycast.
Projectile spawns a projectile object with velocity, and upon hitting it’ll apply damage.
Cursor means you spawn(or do the skill) where your mouse is. Say, you’re charging an AoE spell, or EarthSpike from the selected ground.
Target means it needs a target locked on(there’s a targetting system)
Self, you just hit the button and it’ll do the spell(or charge it first or something, depends).

I thought this is all I need to define how all the spells will fly around and be shot. Apparently not.
Some spells I already have:
FireBolt - Projectile. Shoots a projectile and goes boom on hit. I want the boom to do an area splash.
StormGust - Cursor to select the area. Upon releasing the cast, spawns a sphere with yet another script called FieldEffect that applies water element damage over time in that area.
EarthSpike - Also cursor, and when releasing(if mouse raycast hits ground), spawns a projectile from ground up that hits everything along its path(doesn’t go boom and disappear on first contact).

As I make the spells, the more things I have to hardcode. I don’t mind hardcoding its effects(each spell has a list of effects, on hit, applies all of them). What I’m having problem is how messy each spell acts, casting-wise and, say, how it behaves when it hits. I ended up with a lot of switch-case statements telling how each spell should act. What if I shoot a projectile spell, and it spawns 6 more projectiles on all direction when hit?
Is there a way to make a category on all of them?

Any suggestions?

I have:

  1. PlayerInput class - Detects input.

  2. Hotkey class - What’s currently in the hotkey. I make this because it relates to the hotkey GUI, and other things that can also be assigned to the hotkey slot. This together with PlayerInput determines what each hotkey does when pressed. (I think this is my first mistake. PlayerInput shouldn’t depend on what’s in Hotkey. PlayerInput should only care about what is pressed, and sends the event to the scripts that needs to know it, right?)

  3. ActiveSkills class - It has the name, enum SpiritType element, enum CastType, and a

public static Dictionary<string, ActiveSkill> activesDB = new Dictionary<string, ActiveSkill> ();
  1. PlayerSkills script - One of the variable in ActiveSkills, the castType is this
 public enum CastType{Shot, Projectile, Cursor, Target, Self};

This script goes to the player, it has

public List<ActiveSkill> learnedActives = new List<ActiveSkill>();
public void Shot(string sk, float charge){
// Basically the typical gun type. Sends raycast, etc. Works properly.
}
public void Projectile(string skillname, float charge){
// Instantiates, or get from pool, some sphere with velocity. The projectile has a script called projectile
}
public void Target(string skillname, GameObject target, float charge){
// Skills that need target. Directly calculates damage on the target
}
public void Self(string skillname, float charge){
// Doesn't need target. Aura spells, self buff, etc.
}
  1. Projectiles script - Attached to projectiles that is shot. It contains the skill name, the charge when it is shot
    Again, in here, depending on the skill of the projectile, I have to hardcode each behavior. FireBolt disappears upon hit, while the projectile for EarthSpike doesn’t(can multi hit along its path).

  2. Attribute class - Contains float variables like str, agi, castSpeed, movementSpeed, with appropriate get/set. For example, like so:

private float _HpRegen;
    public float HpRegen{
        get{ return _HpRegen + (float)(_Vit / 15); }
        set{ _HpRegen = value - (float)(_Vit / 15); }
  1. Entity class - which has Attribute myStat = new Attribute();
    This is also where most of the damage gets calculated. I’m also having a hard time figuring out how to make, say, an aura effect. Lets say it’s a burning aura effect. Does it go here, and apply damage to everything around the player, because this is also where I keep track of status effect like burn, frozen, etc?

I only started coding casually around beginning of this year. I’m aware that I’m missing a lot of basic, fundamental knowledge about coding. I’ve been reading around this skill stuff(haven’t found the one specifically related to my problem. Mostly about skill composition and effects, which I think I can figure out), and have been trying to apply using interface, but I just can’t figure out where’s a good place to do that.

For instance, I thought I could put an interface called ICast, and have each skill be categorized by their castType(so I’ll have a class called Projectile, Shot, etc, then in the skillDB.add(“FireBolt”, new Projectile(“FireBolt”, Fire, etc)). And then have all of those class implement ICast. But then I’ll need a target GameObject parameter for Target skills, vector3 for Aoe, etc, so I don’t know where the interface could come in handy.

I could clean up a bit here and there, but I really think I’m approaching this in a very amateur-ish way. I’m ready to learn something new, and possibly remake everything from scratch if only someone can point me on the right direction.

Thanks!

2 Likes

Here are some older posts about data-driven skill systems:

Data-driven is the way to go. You have a good instinct to want to avoid hard-coding.

1 Like

Thanks for the reply!

After checking them out, I still think some skill behaviors need a very unique code that I have to hard-code them.
I guess I understand data-driven is awesome. I’ve played around with them in the past. There’s even some cases where I just use a bunch of floats in a skill (param1, param2, param3, etc), and let the code figure what each represents. 1 of the float handles the duration, where > 0 is damage*time(basic duration), 0 is instant damage, < 0 is the damage/time, etc.

However, the kind of skills I want to make is something like Dota 2’s skill(there are several other games with complex skills too of course).
For instance, a hero called Witch Doctor has a skill called Paralyzing Cask. It requires target(enemy), and upon casting, it’ll throw a projectile to the target. The projectile will stun the enemy for 1 second. The projectile will bounce to nearby enemies within 200 range for a maximum of ~8 bounce(depending on skill) or if there’s no other valid target within range.

There’s 100+ heroes in Dota, each has 4 skills. Out of all those skills, I think there’s only ~3 skills that has this bouncing mechanics.
If I hard code this, then these ~3 skills will benefit from the function I’d be writing. But if I don’t hard-code this, then I should have a way to re-call a SpawnProjectile function after the skill hits a target.
If I really go with the data-driven approach, how will the code look like so that it can also understand that some skills will call SpawnProjectile again after hitting a target, and have an int i-- after every bounce or something?

Cheers!

1 Like

The key is to define as little as possible in code. Unity provides a good way to do this using ScriptableObjects. I provided some example code in this post: Skill systems and other data-rich structures: inheritance or database?

The way to minimize code is to split out functionality into small, abstract pieces. You’re already doing this to some degree. For example, instead of hard-coding in your Projectiles script the different things that can happen when the projectile hits, give it a slot to which you can assign another component that does something on hit. This way, your Projectiles script doesn’t need to know anything about hit behavior. When the projectile hits, it just passes the message along to the hit handler. This lets you add new types of hit handlers without having to touch any code in the Projectiles script.

Then apply this principle to every part of a skill – how it targets, how the player triggers it, etc.

Also, don’t create subclasses. I don’t see that you are; this is a just a suggestion to keep in mind. Instead, try to make things modular like described in the paragraph above.

I’m still not getting it 100%.

The way I understand it, lets just say I have a skill called WeirdShot. Maybe I already created it, it’s already in the DB, etc.
Lets say I want this skill to be cast like a gunshot(raycast in mid of screen), but upon hit it’ll spawn a projectile upwards.

Skill WeirdShot’s variables:
CastType.Shot
Param1 = 5;
Param2 = 2;

Then I have a GameObject called Skiller that handles all raycasting, all instantiating of projectiles, etc. When the raycast hits, it calls Skiller’s OnHitTarget and sends param1 and param2.
The function’s like so:

OnHitTarget(int param1, int param2){
        switch(param1){
        case 1: do another shot
        case 2: spawn explosion area
        case 3 4 5: spawn projectile
            switch(param2){
            case 1: vector3.left
            case 2: vector3.up
            }
        }
    }

Or am I completely missing the point? This is not how I’m gonna actually code it, but just to show that what will determine how the projectile will behave after hit depends on the data it brings(in this case, 5 and 2, whatever that means), and the one that’ll understand what 5 and 2 means is the OnHitTarget function in Skiller(and Skiller probably also have other functions like OnDestroyed, etc)

On another example, so the Projectile script would only have functions such as OnCasted(calls Skiller’s OnCasted with corresponding parameters), OnColliderEnter(calls Skiller’s OnHitTarget), etc.

Is this what you mean?
Sorry, I get skills having weird effects, but somehow having skills doing weird behaviors is hard for me to understand. Especially because some skill doesn’t actually spawn a projectile(it’s not a projectile skill), hits a target instantly, and so on. The only way a, say, Self skill end up casting 20 projectiles around, or a Target skill end up casting something else on skill(like, Drain, that after damaging the target, casts some kind of heal back to caster) can work, in my understanding, with data-driven things, is something like this.
Cheers,

Recently, i was looking at this issue, and found Dependency Injection.

With this, you can do:

using UnityEngine;
 
public class WeaponControl : MonoBehaviour {
 
    public class Dependency{
     
        public virtual void Start(){}
        public virtual void Update(){}
        public virtual void FixedUpdate(){}
        public virtual void LateUpdate(){}
        public virtual void OnCollisionEnter(){}
        public virtual void Destroy(){}
        //etc...
     
    }
 
    [System.Serializable]
    public class FireBold : Dependency{
     
        //Variables
        public float damage;
        public GameObject prefab;
        public Transform parent;
        public Vector3 position;
        public Quaternion rotation;
        public KeyCode fireKey;
        // etc...
     
        public override void Start(){
         
            GameObject obj = (GameObject)Instantiate(this.prefab);
            obj.transform.parent = this.parent;
            obj.transform.localRotation = this.rotation;
            obj.transform.localPosition = this.position;
         
        }
     
        public override void Update(){
         
            //Update Stuff;
         
        }
     
        //etc...
     
    }
 
    [System.Serializable]
    public class StormGust : Dependency{
     
        //Create Dependency, as FireBold
        //Variables
        public float damage;
        public GameObject prefab;
        public Transform parent;
        public Vector3 position;
        public Quaternion rotation;
        public KeyCode fireKey;
        // etc...
     
        public override void Start(){
         
            GameObject obj = (GameObject)Instantiate(this.prefab);
            obj.transform.parent = this.parent;
            obj.transform.localRotation = this.rotation;
            obj.transform.localPosition = this.position;
         
        }
     
        public override void Update(){
         
            //Update Stuff;
         
        }
     
        //etc...
     
    }
 
    /*
    [System.Serializable]
    public class EarthSpike : Dependency{
 
        //Create Dependency, as Firebold
 
    }
    */
 
    [Header("FireBold Skills")]
    [Space(10f)]
    public FireBold fireBold;
    [Header("StormGust Skills")]
    [Space(10f)]
    public StormGust stormGust;
 
 
    Dependency[] weapon;
 
    [HideInInspector]
    public int currentWeapon = 0;
 
    int oldWeapon;
 
 
    void Start(){
     
        this.weapon = new Dependency[]{
         
            new Dependency(),//0.None
            this.fireBold,//1.Firebold
            this.stormGust//2.StormGurst
            //etc...
         
        };
     
        this.oldWeapon = currentWeapon;
     
        this.weapon[this.currentWeapon].Start();
     
    }
 
    void Update(){
     
        //Verify weapon switch
        if(this.oldWeapon != this.currentWeapon){
         
            //Destroy previous weapon
            this.weapon[this.oldWeapon].Destroy();
            //Create new weapon
            this.weapon[this.currentWeapon].Start();
         
            this.oldWeapon = this.currentWeapon;
         
        }
     
        this.weapon[currentWeapon].Update();
     
    }
 
    void FixedUpdate(){ this.weapon[currentWeapon].FixedUpdate(); }
    void LateUpdate(){ this.weapon[currentWeapon].LateUpdate(); }
    //etc...
 
}

From another script, you access and modified the currentWeapon variable.

Here’s an example project: Skill System Example

I’m not claiming this is the only way, or even the best way, but I think it follows the Unity philosophy of using prefabs and component-based design.

The player has a GameObject named Skillbar. Skills are defined as children:

The skillbar and skills use scripts in these folders:

Each skill has different scripts that, as a whole, define the skill’s behavior. The scripts are generic, nothing hard-coded for specific skills. The Launcher (rocket launcher) skill is below.

  • For aiming, it uses “AimCenter”, which aims at targets at the center of the screen.
  • For triggering, it uses “ClickToCast”, which casts the skill when the player mouse-clicks. This sends an OnFire message to the skill.
  • For shooting, it uses “SpawnWithVelocity”, which listens for OnFire and spawns a prefab with a velocity.

The prefab that it spawns is a Rocket. The rocket has three behaviors:

  • “Temporary” despawns the rocket after 5 seconds.
  • “CollisionDamage” causes damage to whatever it hits, by sending an OnTakeDamage message.
  • “SpawnCollisionReplacement” destroys the rocket immediately and replaces it with an Explosion prefab. (The Explosion prefab also has a Temporary that despawns it after 1 second.)

So you can see that none of the code is skill-specific. All skill-specific data is managed in the inspector. You can create prefabs of these skills and add and remove them as the player gains and loses abilities.

Here are some of the main scripts, if you don’t want to download the package. (The package has a working scene, too.)

Skillbar.cs draws the skill bar and lets you select the active skill.

using UnityEngine;

public class Skillbar : MonoBehaviour {

    private Skill[] skills;

    private Skill activeSkill;

    void Start() {
        skills = GetComponentsInChildren<Skill>();
        SetActiveSkill((skills.Length > 0) ? skills[0] : null);
    }

    public void SetActiveSkill(Skill newActiveSkill) {
        activeSkill = newActiveSkill;
        foreach (var skill in skills) {
            skill.gameObject.SetActive(skill == activeSkill);
        }
    }

    void OnGUI() {
        GUILayout.BeginHorizontal();
        foreach (var skill in skills) {
            GUI.color = (skill == activeSkill) ? Color.yellow : Color.gray;
            if (GUILayout.Button(skill.name, GUILayout.Width(64), GUILayout.Height(64))) {
                SetActiveSkill(skill);
            }
        }
        GUILayout.EndHorizontal();
    }
}

Skill.cs is really just a placeholder in this example. The other scripts compose the actual functionality.

using UnityEngine;

public class Skill : MonoBehaviour {

    public GameObject target;

}

AimBase.cs provides basic targeting functionality.

using UnityEngine;

public class AimBase : MonoBehaviour {

    public LayerMask layerMask = 1;
    public Texture2D reticle;

    private Skill skill;

    void Awake() {
        skill = GetComponent<Skill>();
    }

    public virtual Ray GetAimRay() {
        return Camera.main.ScreenPointToRay(new Vector3(Screen.width / 2f, Screen.height / 2f));
    }

    void Update() {
        RaycastHit hit;
        if (Physics.Raycast(GetAimRay(), out hit, 100f, layerMask)) {
            skill.target = hit.collider.gameObject;
        }
    }

    void OnGUI() {
        DrawReticle();
        DrawTargetName();
    }

    public virtual void DrawReticle() {
        if (reticle == null) return;
        var rect = new Rect((Screen.width - reticle.width) / 2, (Screen.height - reticle.height) / 2, reticle.width, reticle.height);
        GUI.DrawTexture(rect, reticle);
    }

    public virtual void DrawTargetName() {
        if (skill.target == null) return;
        var size = GUI.skin.label.CalcSize(new GUIContent(skill.target.name));
        var rect = new Rect((Screen.width - size.x) / 2, ((Screen.height - size.y) / 2) - 50, size.x, size.y);
        GUI.Label(rect, skill.target.name);
    }
}

AimCenter.cs is really just another name for AimBase, since AimBase by default aims in the center of the screen.

using UnityEngine;

public class AimCenter : AimBase {
}

AimCursor.cs overrides the targeting position and the reticle GUI position. This is for skills that aim where the mouse cursor is pointing.

using UnityEngine;

public class AimCursor : AimBase {
  
    public override Ray GetAimRay() {
        return Camera.main.ScreenPointToRay(Input.mousePosition);
    }

    public override void DrawReticle() {
        if (reticle == null) return;
        var rect = new Rect(Input.mousePosition.x - (reticle.width / 2), (Screen.height - Input.mousePosition.y) - (reticle.height / 2), reticle.width, reticle.height);
        GUI.DrawTexture(rect, reticle);
    }
}

ClickToCast.cs implements one way to cast the skill (that is, send “OnFire”).

using UnityEngine;

public class ClickToCast : MonoBehaviour {

    void Update() {
        if (Input.GetMouseButtonDown(0)) {
            SendMessage("OnFire");
        }
    }
}

SpawnWithVelocity.cs responds to OnFire by firing a projectile.

using UnityEngine;

public class SpawnWithVelocity : MonoBehaviour {

    public GameObject prefab;
    public GameObject origin;
    public float force = 500f;

    void OnFire() {
        var projectile = Instantiate(prefab) as GameObject;
        projectile.transform.position = origin.transform.position;
        projectile.transform.rotation = origin.transform.rotation;
        projectile.GetComponent<Rigidbody>().velocity = origin.transform.TransformDirection(Vector3.forward * force);
    }
}

If you added RaycastEffect.cs or SpawnAtCursor.cs instead, it would change the way the skill handles OnFire.

CollisionDamage.cs gets added to projectiles (not to skills). When it collides, it sends “OnTakeDamage”. You could make this more generic by allowing the designer to specify the message that it sends, instead of hard-coding “OnTakeDamage”.

using UnityEngine;

public class CollisionDamage : MonoBehaviour {

    public float damage = 1;

    void OnCollisionEnter(Collision collision) {
        collision.collider.SendMessage("OnTakeDamage", damage, SendMessageOptions.DontRequireReceiver);
    }
}

The rocket has CollisionDamage.cs and SpawnCollisionReplacement.cs, which replaces the rocket with another prefab – in the rocket’s case, an explosion. It also has Temporary.cs, which despawns the rocket after 5 seconds, in case it doesn’t hit anything.

The StormGust spell uses AimCursor and SpawnAtCursor. It spawns a Storm prefab, which has a Temporary to despawn it after 5 seconds.

8 Likes

Wow. That looks like what I’m looking for. I’m currently re-making everything from scratch, as I was building things as I learn things(huge mess everywhere).
Good thing I haven’t completely re-made the skill system when I saw this. Still playing with the database/scriptable object thing and I think I know what I’m gonna do to finish it.

Thanks!
I guess the hard thing about newbies learning things we google up is, we’re just not completely familiar with some terms and “common practices” and stuff.
For instance,

I never heard about this, and that this is (one of) a recommended way of designing codes.
Maybe I’ll have to look into other people’s finished projects and learn from those if I wanna improve more…
But for now… I need to finish something, at least a prototype!

Thanks again. I’ll reply here again if I run into some problems.

Hi,

Sorry to dredge this up again but I’m at the point where I have an instance of the spell object attached to one of the skill slots on my player. So the way I’ve interpreted this is essentially as an inspector driven cherrypicker of scripts? So in this example the structure of the skill should be to give options to the player and then add the script components based on those choices? So in your example would this manifest itself as a method in the script which Initialises as a GameObject and then uses a series of if / switch statements to add script components to that object or is there a more eloquent method to achieve that part?

Hi - The purpose of this design is to avoid if / switch statements and instead compose complex behavior out of several small, relatively-independent components. Take the Rocket Launcher skill above, for example. The screenshot of the Inspector shows that it’s composed of three small components:

  • AimCenter
  • ClickToCast
  • SpawnWithVelocity

Each of these components basically does its job in isolation. The AimCenter component just aims at the center of the screen; it doesn’t care what happens before or after that. The SpawnWithVelocity component just spawns a prefab at the aim point; it doesn’t care how the aim point was determined or how the shot is triggered.

That said, a lot of this was just spitballing and proof of concept from a couple years ago. Nowadays I think a lot of people would opt for ScriptableObjects instead, as in Richard Fine’s recent video:

https://www.youtube.com/watch?v=VBA1QCoEAX4

However, that’s just a GameObject vs. ScriptableObject thing. The important idea is composing larger behavior out of small, self-contained, reusable logic, and leveraging the Unity editor to do it without having to hard-code things.

1 Like

Thanks Tony,

I understood that as far as it trying to remove the idea of complex behaviour in that way so I was unsure on how to use the scriptable object to add / utilise the component monobehaviours. I’ll watch the video shortly. I just also wanted to say thank you so much for posting all of this, it’s been a great help to me.

Oh one more question. How would you go about asigning variables with this method (i.e. amount of damage or similar)?

Each ScriptableObject asset will be a separate instance.

If they represent things like bullets, this makes sense:

public class Bullet : ScriptableObject {
    public float damage;
}

You could have a PistolBullet asset that does damage 2, ShotgunShell asset that does damage 4, Rocket that does damage 10, etc. When you create each ScriptableObject or GameObject, just assign a value to damage.

But you can also go overboard with the granularity of composition. This would probably be a bad idea:

public class Damage : ScriptableObject {
    public float amount;
}

public class Bullet : ScriptableObject {
    public Damage damage; // Points to another ScriptableObject representing damage.
}

You’d have to create separate assets for damage 1, damage 2, damage 3, etc., and assign them to the bullet assets, which would be overkill.

So you’re saying that I still add these to the Skill scriptable object, assign them in the inspector and then pass that variable to the script after it’s instantiated? Also I’ve been trying to write the custom editor so that I can have an objectfield for each script type I need to add to my scripts but I want to be able to limit it to only picking scripts which inherit from the parent class. Is there a way to do that as far as you know? It would feel a bit incomplete to me if I had variables for each script but it allows me to pick from any monoscript… I’ve tried things such as:

//Skill Script
GenericTarget targetScript;

//Editor Script
mySkill.targetScript = EditorGUILayout.ObjectField(mySkill.targetScript,typeof(GenericTarget),false)asGenericTarget;

but obviously this just shows no results. Any ideas how I should approach this? Or am I just being too picky in wanting it to be neat like that?

That should work. It should show a field that only allows you to assign ScriptableObject assets of the GenericTarget type or subclasses.

They’re MonoBehaviours (because they’re the components) not ScriptableObjects. The field in the inspector says None(GenericTarget) but when I click it there is nothing under assets. I have several MonoBehaviours which inherit from it and then the original GenericTarget script as well. Sorry, I’m not just looking to be given the answer, I did a lot of googling trying to see how I could do this but I haven’t found anything that works for me yet.

I’m not sure I follow you 100%. If you’re talking about using the bullseye picker on the right side of the ObjectField, it will only show in-scene GameObjects that have a GenericTarget (or subclass) component. It won’t show prefab asset files. However, it will only allow you to assign prefab asset files that have a GenericTarget. So I think the answer is that you can’t get exactly the behavior you want.

However, another approach would be to call FindObjectsOfTypeAll() in OnEnable to generate a list. Then show it in an EditorGUILayout.Popup instead of ObjectField.

Fair enough, sorry I should’ve uploaded images, this is what I was referring to. Maybe I misunderstood but I thought the point of this design pattern was to create the snippets of code which represent the component parts of a skill. Then attaching the scripts to the scriptable object which would then (on demand) instantiate a gameObject instance of the skill and then add the script components to it? I was expecting (using my code) to be able to look at the asset instances of the scripts and then assign them to the variables on the scriptable object. I’ve probably misunderstood something fundamental though. Here’s what I’m getting anyway:

I’ll try your other method of populating them, that sounds interesting. Thanks! :slight_smile:

For some reason I had very similar discussions with a couple other people yesterday, too, and I’m afraid I don’t remember which approach you’re taking – ScriptableObjects, GameObjects (prefabs), or some mix of the two.

The nice thing about GameObjects (and thus prefabs) is that you can easily add as many components (i.e., instances of MonoBehaviour) as you want. For a Fireball skill, you can start with an empty GameObject, and then add small, narrowly-focused scripts for each element of the skill (targeting, triggering, reducing mana, instantiating a fireball projectile, etc.)

The challenging thing about prefabs is that, apart from simply referencing them, you usually need to have an instance in the scene to invoke its methods.

The nice thing about ScriptableObjects, on the other hand, is that you don’t have to instantiate a copy to invoke methods.

However, it’s not as easy to add elements. You could manually maintain a list of skill elements, or you could create variables for the different types of elements. For the latter, it might be something like:

[CreateAssetMenu]
public class Skill : ScriptableObject {
    public Targeting targeting;
    public Triggering triggering;
    public Firing firing;
}

public class Targeting : ScriptableObject {}
public class MouseTargeting : Targeting {}
public class ScreenTargeting : Targeting {
    public Vector2 screenPosition;
}

public class Triggering : ScriptableObject {}
public class KeyTriggering : Triggering {
    public KeyCode fireKey;
}

public class Casting : ScriptableObject {}
public class ProjectileCasting : Casting {
    public GameObject projectilePrefab;
    public float speed;
}

To create a Fireball skill that launches a fireball projectile from the center of the screen when you press ‘F’, you might:

  • Create a new Skill asset named “Fireball”.

  • Create a new ScreenPositionTargeting asset named “ScreenCenter” and set screenPosition to (0.5,0.5).

  • Create a new KeyTriggering asset named “KeyF” and set fireKey to KeyCode.F.

  • Create a new ProjectileCasting asset named LaunchFireball, assign the fireball prefab to projectilePrefab, and set the speed.

  • Finally, assign ScreenCenter, KeyF, and LaunchFireball to the Fireball asset.

If you didn’t want separate asset files for the skill elements (ScreenCenter, KeyF, LaunchFireball), you could write a custom editor to create ScriptableObjects without saving them as asset files, assign them to the Fireball asset, and use AssetDatabase.AddObjectToAsset to add them to the Fireball asset itself. This way you have only one asset file instead of four. However, this also means you can’t re-use ScreenCenter or KeyF. You’d have to reproduce them for every skill that uses them.

@TonyLi Thanks for outlining the composition approach. It sounds promosing but there aren’t many other examples or tutorials out there.
Anyway I’m giving it a go and have 3 questions:

1/ How’d you manage spell’s mana cost and player mana usage?
Should every button/spell have a separate script with a ‘manaCost’ property that is subtracted from the player’s static property manaPool every time the button is clicked?

2/ How’d you manage healing (+health) and damage (-health) friendly units?
Let’s say the player can heal friendly units. I now have a Healable.cs (like your Damagealbe) on very unit with a Heal() class that waits for messages to be sent form CollisionHeal (like your CollisionDamage) to add health points to health. How it needed to be modified to access health when friendly units take damage?

3/ How’d implement something like a spell cast time or spell cooldowns ?

Thanks