So I want to come up with an Action/Spell system akin to World of Warcraft. Essentially, I believe the internal mechanic is that each button runs a macro such as “/cast Seed of Corruption and cackle maniacally”.
Obviously /cast would trigger the function to actually perform the action. But that bit, calling it by name, “Seed of Corruption” is what puzzles me. I’d like my system to be very flexible and open-ended, so that all it would take is to write the script for what the new action does, and bam - integrated.
In my mind, this would be to create an Action class, and have all actions inherit from it, or something like that, and somehow call the class by name through a generic. I’m not all that familiar with how generics work though…
I could spend a lot of time experimenting and trying new concepts, but in the end I figure it would be more practical (and stable for that matter) to see how others would handle a system like WoW has.
What suggestions would you have for something like this?
From what you are saying you are thinking of an Action class, and that is exactly how i would solve this too.
In this action class i would make a virtual or abstract function with a name like Perform(), which performs the action, you then override this in derrived classes of action. That way you can easily create a new one, and you just have to create the button for it and write the derrived class.
I don’t have any experience with making exactly this myself tho, but I have a strong feeling it will work out.
This is exaclty what I do in my framework (link in signature if you want to see the system in action, including actions)…
The stub of my action class goes like…
/// <summary>
/// This class describes the base functions necessary to create an ability.
/// </summary>
public abstract class Ability : ChangeSource
{
/// <summary>
/// The icon texture of the ability. This field is optional.
/// </summary>
public Texture2D Icon { get { return _Icon; } }
[SerializeField]
private Texture2D _Icon = null;
/// <summary>
/// A variable to store the effect. This field is optional.
/// </summary>
public GameObject Effect { get { return _Effect; } }
[SerializeField]
private GameObject _Effect = null;
/// <summary>
/// The cast time of the ability in seconds.
/// </summary>
public Single CastTime { get { return _CastTime; } }
[SerializeField]
private Single _CastTime = 1;
/// <summary>
/// True if the ability can be cast while the unit is moving.
/// </summary>
public Boolean CastMobile { get { return _CastMobile; } }
[SerializeField]
private Boolean _CastMobile = false;
/// <summary>
/// The cast range of the ability.
/// </summary>
public Single CastRange { get { return _CastRange; } }
[SerializeField]
private Single _CastRange = 5f;
/// <summary>
/// The cooldown of the ability in seconds.
/// </summary>
public Single CastCooldown { get { return _CastCooldown; } }
[SerializeField]
private Single _CastCooldown = 1f;
/// <summary>
/// True if the ability requires a target, false otherwise.
/// </summary>
public Boolean RequiresTarget { get { return _RequiresTarget; } }
[SerializeField]
private Boolean _RequiresTarget = false;
/// <summary>
/// Called by the engine to check whether an ability is usable at all.
/// </summary>
/// <param name="caster">The unit that will cast the ability.</param>
/// <returns>True if the ability can be used, false otherwise.</returns>
public virtual bool OnCheckUse(Behaviors.Unit caster) { return true; }
/// <summary>
/// Called by the engine to check whether the ability is usable on a target or location.
/// </summary>
/// <param name="caster">The unit that will cast the ability.</param>
/// <param name="target">The target unit of the ability.</param>
/// <param name="position">The target position of the ability.</param>
/// <returns>True if the ability can be used on the target, false otherwise.</returns>
public virtual bool OnCheckUseOnTarget(Behaviors.Unit caster, Behaviors.Targetable target, Vector3 position) { return true; }
/// <summary>
/// Called by the engine when the ability has begun to be cast.
/// </summary>
/// <param name="caster">The unit that will cast the ability.</param>
/// <param name="target">The target unit of the ability.</param>
/// <param name="position">The target position of the ability.</param>
/// <returns>An optional user-defined tag that is *not* synchronized across clients.</returns>
public virtual System.Object OnBeginCast(Behaviors.Unit caster, Behaviors.Targetable target, Vector3 position) { return null; }
/// <summary>
/// Called by the engine when the ability is being cast or channeled.
/// </summary>
/// <param name="tag">An optional user-defined tag that is *not* synchronized across clients.</param>
/// <param name="time">The time left before the channel is complete.</param>
/// <param name="caster">The unit that is casting the ability.</param>
/// <param name="target">The target unit of the ability.</param>
/// <param name="position">The target position of the ability.</param>
/// <returns>An optional user-defined tag that is *not* synchronized across clients. The tag will be passed along to methods.</returns>
public virtual System.Object OnChannel(System.Object tag, Single time, Behaviors.Unit caster, Behaviors.Targetable target, Vector3 position) { return tag; }
/// <summary>
/// Called by the engine when the ability channel has been interrupted.
/// </summary>
/// <param name="tag">An optional user-defined tag that is *not* synchronized across clients.</param>
/// <param name="caster">The unit that was casting the ability.</param>
/// <param name="target">The target unit of the ability.</param>
/// <param name="position">The target position of the ability.</param>
public virtual void OnInterruptedCast(System.Object tag, Behaviors.Unit caster, Behaviors.Targetable target, Vector3 position) { }
/// <summary>
/// Called by the engine when the ability channel has been completed.
/// </summary>
/// <param name="tag">An optional user-defined tag that is *not* synchronized across clients.</param>
/// <param name="caster">The unit that was casting the ability.</param>
/// <param name="target">The target unit of the ability.</param>
/// <param name="position">The target position of the ability.</param>
/// <returns>An optional user-defined tag that is *not* synchronized across clients. The tag will be passed along if the ability launches a projectile.</returns>
public virtual void OnEndCast(System.Object tag, Behaviors.Unit caster, Behaviors.Targetable target, Vector3 position) { }
}
Note that change source is derived from UnityEngine.ScriptableObject which means all fields set as [SerializeField] can be edited in-engine using the unity inspectors, making for rapid tweaking balancing!
Sounds from everyone that I’m probably on the right track with this!
I do wonder about this post though; if the goal is serialization, couldn’t one simply use the [System.Serializable] attribute? Or was it perhaps another benefit that ScriptableObject helps with?
When a person is considering using inheritance for a function like Perform() you are a bit stuck in the OO mess.
Inheritance, if used, should only be is-a relationship, which is not what we are trying to accomplish here.
The easiest solution is the one i wrote, make each script have unique function name for the perform function that matches a striped version of the button name. Then the code i posted works, no inheritance or interface needed. The best thing about this is that we can choose to have the function “Seed of Corruption” in a separate script or grouped with other such funcitons in one single script, it doesn’t matter seance we are using SendMessage().
If you want a more rigid solution you can use interface and place each function in it’s own script that implemented the interface and use either SendMessage or StartCoroutine depending on if you want to have references to the function scripts in the calling script or just use sendMessage.
Is that to say that all action libraries would have to be attached to the character?
Inheritance is useful in more than just that way. With an Abstract class, I can always make sure the generic function refers to a class that “is an” Action. And because all Actions have the Perform() function, I can always be sure I’m calling it correctly.
I figure this would be a more useful method than having to send messages to monobehaviour, unless I’m mistaken in your meaning?
If the action libraries are built decoupled you could probably make them only appear once in a global GameObject where you make references to them in the characters Start functions.
But how in detail would you target the correct class? Would you have some form of array where you search for objects names and have it match SeedsOfCorruption and then call perfom on it? Then all new actions you built must be in this array, this does not strike me as flexible and extensible.
With the SendMessage solution you only need to have the function somewhere on the gameObject you are calling it on. You only need to add scripts with the correct name. You could go further and even base the GUI buttons on how many of these scripts there are. This is sort of what i have done on my hobby game project.
I guess we need to know more of what you define as useful and how you envision to target the correct class to make the best derision.
I would reference the class through a generic function. While I still have a lot to learn about them, essentially I should simply be able to reference <“Seed of Corruption”>, use the generic function to determine if the referenced class is an Action, and then all that’s left to do is call .Perform(); from it.
The only real downside I’m seeing is that skill names will be a tad limited this way, since it goes by class name. Oh well, that shouldn’t be too hard to circumvent.
I am really unsure on how you mean. How do you meant what the generic function would do? Search a gameObject for the class?
Oh comon, i meant that it is a bit much bringing on the whole inheritance chain when the goal is to call a function. Inheritance is a mayor problem in modern OO programs, it is used for everything when one should really use containment or mixins. A simple interface or a SendMessage based on the button would work in this case, no need to bring in the whole inherit tree.
Ok, if it were just for calling a function you were totally right, the thing is it is for an action, where each action can have its own variables and sometimes more then one function. Also it might be possible to have some unequipped actions, where your action bar script gets very messy fast.
Ill end this flame war with this: If it is for the easy solution, i would totally give youright, but if were talking about an extendible approach you have got to go object orientated, as it is made for extendibility.
It’s called composition, not containment. Secondly, you’re pretty much flat out wrong. Yes for the simple example of calling a specific function named something, sure go with SendMessage. For anything more complex you will just make your code a big pile of cross linked functions calling each other back and forth with no real structure.
Well, I’ve played around a little with ScriptableObject and I’m a bit more familiar with how generics work.
I’m not entirely sure I want actions to be actual objects themselves, but perhaps some sort of static reference so I don’t have so many objects laying around everywhere. Since the Perform() function is the most important part, this could, perhaps, somehow be accessed through the generic function I mentioned before.
This could mean setting a character’s actions would be as simple as creating a list of strings, each referring to a different action.
instead of a list of think you should consider either using an enumeration, or a list of a base Action class. Then you dont have to look for the class belonging to that string, but you can access it directly.
I meant composition but didn’t find the word at the time.
I find you critique unsolid and unprofessional, i mean you haven’t even said in concrete terms how my solution would be bad.
You can have structure withouth solving all problems with inheritance, you can have structure with functions, like functional programming.
How would it automatically turn in to spaghetti code?
The GUI analyse from somewhere (file, character class file, action array whatever ) and draw up all the buttons, then when you click a button you send a SendMessage() to the gameObject that has the function. If the function is small you can have them grouped in a single file, if they require more code they can be in a separate script on that gameObject. This works because my solution is decoupled.
If you are concerned with many objects i recommend using a single GamePlay gameObject on your scene that contains all actions, and make reference to these objects from your characters scripts.
@sirex , i would be more concerned at the professionality of your code.
also why not to use your easy solution:
i find that when i need to work with the same code for a bigger project, i want to have it as extendable as possible as i may change my mind on decisions halfway. Also I want other people to be able to easily understand my code, in object orietated programming it is all about encapsulating your code, really.
I’m trying to design it with modularity in mind, such that I could easily come in any time and add new scripts with new actions without having to change any of my old code.
I designed my character state system to be component-based because of this; the monobehaviour component will have full access of monobehaviour to work with, and is easily overridden with a new state. This way, if I ever wanted to add say, a skateboarding state, I would only have to write up a single new script.
Likewise, if I wanted to add a new spell like “Fireball”, I would simply have to create a Fireball script.
Huh, I seem to remember back when I would mod Oblivion, they had a spot in the editor where you could select a script to run when the spell was cast.
Now that I think about it, perhaps making actions objects, themselves, wouldn’t be so bad. I’ll go back over all this in my head, with that option still on the table.
On what grounds would you be concerned?
And how is not a generall SendMessage encapsulating? You can litteraly implement the Action part how you want seance the SendMessage don’t care.