I’ve gotten to the point in my development cycle where I need to start looking at how skills will be tracked and organized. Right now, the simplest solution I can think of is to make a core SkillBasic.cs class that contains universal features like names and MP cost, build an empty Use() function into it, then make a new class that extends SkillBase for each new skill, overriding Use() to account for different skills having drastically different utilities. If I create a single delegate or reference for each keybind, and make that keybind’s Input.GetButton simply find the referenced class and execute mySkill.Use(), I could swap things in and out and generally have an easy way of things.
The problem I have with this is that it feels perilously close to hard-coding: many good ARPGs have upwards of 50 skills, sometimes more, and curating 50 subclasses or more as the game evolves sounds like a nightmare.
Given that, is there an obvious, better structure I could be using here? I’ve briefly flirted with the idea of making a database that keeps some array SkillBasic[ ] which contains a variation of SkillBasic for each unique skill in-game, but that would require me to create a custom inspector so I didn’t have to generate all of my skills at runtime, and it doesn’t let me use overrides to change the skill’s action the way subclasses would.
Commands, or how to execute steps, is what I would use inheritance for.
Something like an inventory system will definitely use both. Eg: I load from a file the costs of each item, which icon id to use, how “strong” the item is, etc. But for a health potion, the actual adding health to the player is done with inheritance.
That makes a lot of sense… how do you handle managing large libraries of commands though? One subclass per interaction type (healing, attacks, buffs) simplifies things somewhat, but at the end of the day you’re still making a whole bunch of classes to handle minute variations in function.
Yup! Have a heal spell, physical attack, buffs… You can probably have one class for “spells” then extend that further for “heal spells” and “damage spells” which is probably good enough at that point. You might need to extend those further if you have like “Heal over Time” as well as “Instant Heals”.
Another thing you can do is use the command pattern. So instead of having 100 classes, you really just need 100 CastSpell delegates you can assign to a specific Spell object.
I would do a few spells (at least 3) first, as separate classes if you have to, then start trying to figure out what’s common among them and start moving code to base classes.
Hmm, that makes complete and utter sense, but are delegates serializeable? The main reason I’ve been hesitating to use them is because I was under the impression that they were not, meaning I would have to do a bit of an end-around to save the player’s skill bar arrangement between playthroughs.
You might glean some ideas from plyGame’s Skills system. In plyGame, a skill is just one class (derived from ScriptableObject) that has data fields that define how the skill works, such as:
Name
MP cost
How it’s activated (when the player hits a button or performs an action, automatic in response to something, etc.)
How it’s delivered (instant/melee, or spawn a projectile)
Targeting (self, player’s current target, auto-select)
Valid classes of targets
Projectile to spawn
Damage to inflict
etc.
Following this model, a skill would simply be a data file (asset) in your project. The skill class may be a little bigger than a single-skill class in order to handle everything generically, but overall it’s much simpler to maintain because it’s just one class, no delegates, no subclasses.
Characters would have a list of skills (e.g., List skills). When the character learns a new skill, assign the asset to this list.
Ooh, that is a great resource… how do they actually check the flags they’re setting, does it execute some kind of long conditional list every time you activate it?
switch (targeting) {
case Targeting.Self: HandleTargetSelf();
case Targeting.CurrentTarget: HandleTargetCurrentTarget();
case Targeting.AudoSelect: HandleAutoTarget();
}
because eventually the data has to hit the code at some point. Just try to keep it as decoupled and modular as possible to avoid interdepencies between these switch statements.
One last question occurs as I’m writing my skill class, what is the best way to organize and manage things in the project? Right now I’m just creating an empty gameobject, calling it Skill Database, and attaching each instance of Skill to it, but that seems like a fugly and slightly silly way to do it.
It depends on your project, but I’d probably define a SkillDatabase class that inherits from ScriptableObject. This way you can make a freestanding asset that doesn’t have to be attached to a GameObject in the scene. Because it inherits from ScriptableObject, it’s serializable and you can edit it in the Inspector view. (Jacob Pennock wrote a good ScriptableObject tutorial.)
Ultimately you’ll need some reference to a SkillDatabase asset at runtime. You could attach a MonoBehaviour with a SkillDatabase variable to a singleton GameObject, or reference it in a (static?) class that isn’t a MonoBehaviour, or just add it as a field in a MonoBehaviour for your character.
To roughly sketch out what I’m talking about:
enum Activation { Button, Action, Automatic };
enum Targeting { Self, CurrentTarget, AutoSelect }; // et cetera for other settings.
//=== Individual Skills: =============================
public class Skill : ScriptableObject {
public string skillName = string.Empty; // I always initialize variables.
public float mpCost = 0;
public Activation activation = Activation.Button;
public string activationButton = "Fire1";
public Targeting targeting = Targeting.CurrentTarget;
// etc.
public bool isActive = false;
public void Update(MonoBehaviour parentMonoBehaviour) {
if (!isActive && CanActivate()) {
parentMonoBehaviour.StartCoroutine(Use());
}
}
public bool CanActivate() {
switch (activation) {
case Activation.Button: return Input.GetButtonDown(activationButton);
// etc.
}
}
public IEnumerator Use() { // Coroutine so it can do stuff over time.
isActive = true;
// Do stuff based on the settings above.
isActive = false;
}
}
//=== Skil Database: =============================
public class SkillDatabase : ScriptableObject {
public List<Skill> skills = new List<Skill>();
public void Awake() {
// At runtime, instantiate skills so you don't modify design-time originals.
// Assumes this SkillDatabase is itself already an instantiated copy.
for (int i = 0; i < skills.Count; i++) {
skills[i] = Instantiate(skills[i]);
}
}
public void Update(MonoBehaviour parentMonoBehaviour) {
skills.ForEach(skill => skill.Update(parentMonoBehaviour));
}
}
//=== Character MonoBehaviour: =============================
public class Character : MonoBehaviour {
public SkillDatabase skillDatabase = null;
void Awake() {
// At runtime, instantiate a copy so you don't modify the design-time original:
skillDatabase = Instantiate(skillDatabase);
if (skillDatabase != null) skillDatabase.Awake();
}
void Update() {
if (skillDatabase != null) skillDatabase.Update(this);
}
}
Again, this is only a rough sketch, with a couple tricky points (such as instantiating, and coroutines) thrown in. But I just typed it into the reply box. There might be typos.
Holy cow this is exceptional, thank you so much for your help!! I’m going to have a lot of fun dissecting this to work out how to best do things… just to clarify, when you say attach it as a field in a monobehavior, you’re saying that I’m better off making a SkillDatabase variable in a script on my character and setting it to the game’s database, or can I get away with just dragging the script itself onto the player’s object?
Oh wait, I see what you did with the instantiation, ignore me- I was being dense, I forgot that you can’t drag & drop things that don’t inherit from Monobehavior ^^
Hmm, most of this is self-explanatory, but would you mind clarifying two quick things?
Where would the actual creation of new skills occur in the workflow? Would I build some kind of constructor into my Skill class, then create a line in SkillDatabase’s Awake() for each unique skill I want, and pass different values to the constructor there?
What is the purpose of passing the MonoBehaviour argument in Update and Awake? It looks like they’re supposed to point at the gameobject the database is instantiated on (to wit, the player), but I don’t see where they’re actually given their reference, or why you wouldn’t do something like
That’s a good question because I omitted that part. It’s in Jacob Pennock’s tutorial, but you’d create a menu item that calls the static method ScriptableObject.CreateInstance(). Here are shorter, more up-to-date instructions: Grab the ScriptableObjectUtility.cs from the wiki. Then create a script in a folder named “Editor”. For example, you could call it SkillMenu.cs:
using UnityEngine;
using UnityEditor;
public class SkillMenu {
[MenuItem("Assets/Create/Skill")]
public static void CreateAsset () {
ScriptableObjectUtility.CreateAsset<Skill>();
}
[MenuItem("Assets/Create/Skill Database")]
public static void CreateAsset () {
ScriptableObjectUtility.CreateAsset<SkillDatabase>();
}
}
This will add a couple menu items to the Unity editor. When you select them, they’ll create Skill and SkillDatabase asset files.
Skill and SkillDatabase inherit from ScriptableObject, not MonoBehaviour. The StartCoroutine() method is only available for MonoBehaviours. So I passed along the MonoBehaviour to give skills access to the StartCoroutine() method. This was just an embellishment. If the skill’s Use() method can execute and finish right away (for example, to simply spawn a prefab such as a fireball), you can get rid of the coroutine stuff.
So thank you very much for your feedback! In several concise and well-informed posts you’ve cleared up about a hundred questions for me, I really appreciate it! ^^
Hmm this is odd, unity doesn’t like me using CreateAsset() twice, is there something I’m missing that would let me use the member for each menu item I make for ScriptableObjectUtility?
Sorry, I’ve never actually used ScriptableObjectUtility. I’ve always directly called ScriptableObject.CreateInstance<>() and AssetDatabase.CreateAsset().
I assume you’re referring to ScriptableObjectUtility.CreateAsset<>(). But if it’s complaining about AssetDatabase.CreateAsset() instead, make sure you’re providing new assets and unique paths for each call.
If that doesn’t help, please post your code here and any errors it’s reporting.
[MenuItem("Assets/Create/Skill")]
public static void CreateAsset () {
ScriptableObjectUtility.CreateAsset<Skill>();
}
[MenuItem("Assets/Create/Skill Database")]
public static void CreateAsset () {
ScriptableObjectUtility.CreateAsset<SkillDatabase>();
}
What’s giving it fits is having two public static void CreateAsset() functions- the error goes away and the menu creation option remains if I just rename one of them, but I’m not sure if I’m supposed to do that.