Help with building block ability system

I wanted to know if any more advanced programmers could give me some guidance and help me plan how to create a system for abilities using a building block style system. I am a bit unsure where to start and how to create a system like this.

My idea is for every ability to made by combining premade actions and executing them in order, with each action allowing for parameters to be input such as distance, direction, duration, damage, etc.

As an example, a fireball ability would go through a process like this:

  • projectile spawned at (position)
  • move projectile in (direction) for (distance) over (duration)
  • on enemy hit, explode in (radius) and apply (damage) and/or apply (debuff)
  • on terrain hit, (destroy)

Thank you in advance.

I believe you will have to offer a more details on your plans because there are probably any number of ways to implement it (depending).

A system that you can use or a system that you can offer (an app) that permits others to build things by setting these things? 2D? Single-player?

1 Like

At the most basic level this can get distilled down to a collection of a base type of object (either a base class or interface) that expresses the minimum required implementation for each action. Then you simply run through the collection one element at a time.

If each step may take some time to complete, you could use either coroutines or async to do this. Nowadays I would use async as itā€™s more flexible, doesnā€™t require a monobehaviour, and can also run in the same frame (either vanilla C# Tasks, Unity Awaitables, or UniTask (my preference)).

The base interface would be very minimal:

public interface IAbilityAction
{
    System.Threading.Tasks RunActionAsync(CancellationToken ct);
}

Then you can just run through them one by one:

private List<IAbilityAction> _actions = new();

public async Task RunActionsAsync(CancellationToken ct)
{
    foreach (var action in _actions)
    {
        await action.RunActionAsync(ct);
    }
}

How you would design this in Unity can be done a number of ways:

  1. A collection of components of a base type
  2. A collection of scriptable objects of a base type
  3. A collection of plain C# objects serialized with [SerializeReference] with some custom inspector support (my preference).

It just gets tricker when you want to pass information into these actions, or between actions, namely from the runtime/scene realm of things. I would probably do this by having a ā€˜contextā€™ object that contains the information gathered up beforehand for the ability, such as targets, and with the same context reused for each step, so data can be passed between each step.

Whether or not this is the best way to handle an ability systemā€¦ it depends I suppose! The alternative is to code abilities in one go. Ergo, you have just a class for a projectile ability. Though of course, a fireball and icebolt can use the same ability, they will just have different visuals.

Doing it modularly will require more abstraction, more boilerplate, more code to support everything, and so on. But if it works out as expected, you get more reusability, more flexibility. Perhaps worth considering if this project will require this level of flexibility.

1 Like

Apologies for not providing more specifics.
The project is in 3D, online and local multiplayer, two or four player are against each other in an arena using these abilities. AI opponents will also be present, functioning just like players.

Each player will have a team of characters with only one active, in the arena, and controlled at a time. Each character will have a few abilities available to them during battle from a pool of possible ones, unique to that character.

I hope this helps, let me know if additional information is needed.

Thank you for your help and suggestions.

My biggest question is which approach do you think would best suit an online multiplayer environment?

I have limited experience with coroutines and even less with async tasks as I have always found both confusing. I will try learning more about them.

Iā€™ve never heard of UniTask or Awaitables. I assume they are more recent additions/features?

My initial thought on the approach is similar to yours, that being creating an Action class with an execution method and any parameters as variables for this method, and an ability class/scriptable object which contains a list of actions and all the parameters for each action to pass onto them. Understanding and conceiving the exact implementation is the biggest struggle and has made me doubt my understanding and capabilities.

I have worked with scriptable objects a decent bit and the thought crossed my mind of actions being a scriptable object, inside of an ability scriptable object. Although, the idea sounds redundant and not very effective (to me at least).

As you mentioned, passing the information to an action (ex. damage/distance/duration) is likely the most difficult part, especially when every ability is made to be modular. Therefore, every ability will need to store information somewhat differently based on the actions it uses, unless there is a better method of handling this data.

If there is a way for a class to dynamically contain more information while directly associating it with something (ex. an integer for damage is somehow dynamically added and associated to an action when it is added), that would be ideal, but to my understanding, that is not possible or would require A LOT of work to pull off, well above my capabilities.

I donā€™t think information such as target objects/layers will be a big issue. Characters casting the abilities can find what layer they are on and based on that, target the layer of the opposing team, or a similar process.

I do believe this project would benefit greatly from a flexible and modular system like this. Many abilities are planned to be made, upwards of 50 and likely over 100. Additionally, some team members would either struggle greatly or lack the skills needed to ā€œhard codeā€ every ability. This pseudo-drag-and-drop or building-block style would be much easier to understand and design with and can be used in other projects, especially when paired with an editor UI.

Iā€™ve not worked on any multiplayer or networking stuff so I canā€™t answer this question.

Nonetheless I will answer your other questions.

While Unity has supported async code for a while. Awaitables is there own integration to work nicely with the Unity player loop: Unity - Manual: Asynchronous programming with the Awaitable class

UniTask is a third party addon that existed before Awaitables. It existed to add better async support before Unity fully supported it, but even with better support by Unity and the release of Awaitables, itā€™s still an excellent high-performance option with an extensive API: GitHub - Cysharp/UniTask: Provides an efficient allocation free async/await integration for Unity.

This is why the [SerializeReference] approach is my preference. You can reduce the asset bloat by serialising plain C# objects into another object - with polymorphism.

You could also make an action that references an action scriptable object (which itself just SerializeReference-es an action), if you have instances where you want to reuse data.

I mean it could be pretty straightforward to do. It honestly would just need a dictionary.

Something like this could be a starting point:

public sealed class AbilityActivationContext
{
	private GameObject _owner; 
	
	private readonly List<GameObject> _targets = new();
	
	public GameObject Owner => _owner; // the entity that owns/activated the ability
	
	public IReadOnlyList<GameObject> Targets => _targets;
	
	private readonly Dictionary<Type, object> _contextDataRegistry = new();
	
	public AbilityActivationContext(GameObject owner)
	{
		_owner = owner;
	}
	
	public void AddTarget(GameObject target)
	{
		_targets.Add(target);
	}
	
	public void ClearTargets() => _targets.Clear();
	
	public void SetData<T>(T data)
	{
		bool exists = _contextDataRegistry.ContainsKey(typeof(T));
		if (exists == false)
		{
			_contextDataRegistry.Add(typeof(T), data);
		}
		else
		{
			_contextDataRegistry[typeof(T)] = data;
		}
	}
	
	public bool TryGetData<T>(out T data)
	{
		object d = null;
		bool hasData = _contextDataRegistry.TryGetValue(typeof(T), out d);
		if (hasData == true)
		{
			data = (T)d;
		}
		return hasData;
	}
}

There are no doubt more efficient ways to do so; this is just the easiest way to showcase the concept. Overall, not a lot of work.

Though ideally you try to reduce the interdependence between individual actions. They should be, as much as possible, completely standalone.

With designers in mind and that number, then very likely. It depends how much variety in behaviour youā€™re expecting, of course.

If a lot of them are different flavours of the same behaviour, it might not be necessary to go down this path.

2 Likes

Unless Iā€™m imagining things incorrectly this would basically come ā€œfreeā€. You wouldnā€™t be creating building blocks of ā€œactionsā€ necessarily but rather just building blocks. If you need an ā€œenemyā€ you invent such a component and it contains the public data that other blocks can expect to be available.

You might look into graphical languages (for inspiration) as nodes typically expose whatever limited values/functionality is available.

the first step in building these complex systems is to understand coding patterns and how they can be used for game development. fortunately unity had released a good resource about this so you can have a good foundation to build upon

What kind of multiplayer environment are you looking at? If itā€™s competitive and can potentially involve strangers then that makes things significantly more complex as you will need some kind of server authority. On the other hand, if this is mostly cooperative between friends this will be much simpler as you can avoid most of the security boilerplate and just assume everyone is playing fair. And if they arenā€™t wellā€¦ thatā€™s between the player and the friends they choose.

In my own case Iā€™m working on something like this right now. Itā€™s intended to be coop between friends so I donā€™t care about security or hacking. All abilities spawn projectiles and itā€™s simply the stats, and effects that define how it behaves. For example, a melee attack is just a projectile that doesnā€™t move and disappears after a short time, a ground effect is a projectile that doesnā€™t despawn when it strikes a floor, a shield is a projectile that follows the player that spawned it, etc.

When a player spawns a projectile they take ownership of it but all others are signaled to spawn their own projectile locally. They are not networked objects. Positions and timing are most deterministic and donā€™t require anything to be synced over the network except when to despawn. Collisions are also handled locally. When a client decides that something should cause a projectile to despawn (because it collided with something or was dispelled or whatever) that information is synced to all other clients via the server and they all despawn that projectile. This has a few effects 1) Movement and collision response are instant on everyoneā€™s machine. 2) Bandwidth usage is extremely minimal. and 3) Some desyncs will occur but in most cases people arenā€™t going to notice or care too much. Since collisions are handled locally on each machine, they are responsible for tracking their own playerā€™s health as well as sending a signal to the server when an enemy is hit (enemies are, of course, synced over the network).

Is this perfect? Nope. But for a small game with limited appeal and I find it works well enough. I doesnā€™t require me to rent out centralized servers or spin up anything on a cloud that Iā€™ll never make a return on. Each player can simply host locally and it wonā€™t smash their bandwidth usage.

1 Like

Thank you immensely for the information. I have never worked with dictionaries before but will certainly be researching it.

I will spend some time the next few days learning and attempting the build this system. Iā€™ll report my progress here for any interested.

1 Like

It will be a more competitive environment, so there will be server authorization and some level of upkeep costs. I have some experience networking and have some decent understanding of it although I, and hopefully others, appreciate the information nonetheless.

The project will also have a campaign, whether it will have multiplayer functionality (such as seeing other players in the world) is yet to be determined. The importance of mentioning this is I am unsure how local/offline gameplay would be handled when the systems are already built for online. Would I need to make a second version of the systems with online functionality? Iā€™m sure that is not the case, but I simply donā€™t know the answer.

Hello again. After some research and starting to develop/experiment, I noticed that the inspector does not want to show a list of interfaces, which the action classes all use an action interface. Supposedly there is a work around but Iā€™m having trouble finding out how.

So instead, I tried creating an abstract class for actions that all actions derive from. Unfortunately the inspector only shows the list IF the base class is not abstract AND marked with [Serializable]. If both of those are not true it will not show. Additionally, the derived classes do not appear when attempting to add objects to the list from the inspector, marking them with [Serializable] does not change this. I tried using the [SerializeReference] thing you mentioned, but it seems it only applies to fields and not classes so I must not understand something there.

Iā€™m having difficulty finding information on why any of this is or how to get around it other than a simply ā€œitā€™s a limitation.ā€ If you could provide some assistance in understanding and potential solutions, I would be very grateful.

The following is the first version utilizing interfaces.

AbilityScriptableObject

public class AbilityScriptableObject : ScriptableObject
{
    public enum AbilityType { Melee, Ranged, Buff, Debuff, Utility };
    [field: SerializeField] public AbilityType abilityType { get; private set; }

    //List of all actions in the ability
    [field: SerializeField] public List<IAbilityAction> actions { get; private set; }

    public async Task ExecuteAbilityAsync()
    {
        foreach (var action in actions)
        {
            //Implement a better way of execution actions
            await action.ExecuteAction();
        }
    }

Action Interface and Class

public interface IAbilityAction
{
    public Task ExecuteAction();
}

[Serializable]
public sealed class SpawnProjectile : IAbilityAction
{
    public GameObject projectileObject;
    public Vector3 position;
    public Vector3 rotation;

    public async Task ExecuteAction()
    {
        GameObject.Instantiate(projectileObject, position, Quaternion.Euler(rotation));
        await Task.Yield();
    }
}

As I had mentioned, polymorphic serialization needs to be done with the [SerializeReference] attribute, not the traditional [SerializeField] attribute.

You will also need some form of custom inspector support as Unity doesnā€™t have this built in.

I wrote a rough example here: abstract SkillList that can be edited in Inspector - #15 by spiney199

Other addons like Odin Inspector support this out of the box.

Thanks for the help. I understand polymorphic serialization now and thankfully a team member has an Odin Inspector license so everything was easy to set up and works properly.

My new issue is passing some information to an action. While parameters about the action itself function just fine, in this case I want my SpawnProjectile action to create the projectile GameObject at a position and rotation relative to the caster of the ability, but how can I pass the caster GameObject to the action class so it can calculate the position?

[Serializable]
public sealed class SpawnProjectile : IAbilityAction
{
    //Spawns a projectile entity relative to the position and rotation of the caster.
    [SerializeReference] GameObject projectileObject;
    [SerializeReference] Vector3 position;
    [SerializeReference] Vector3 rotation;

    public async Task ExecuteAction()
    {
        GameObject.Instantiate(projectileObject, /*casterObject.transform.position + */ position, /*casterObject.transform.rotation * */ Quaternion.Euler(rotation));
        await Task.Yield();
    }
}

My first idea was to add the caster GameObject as an argument in the ExecuteAction method of the SpawnProjectile class but this causes implementation issues with the interface and also makes the process of executing actions from the ability much more tedious, as each action would need to be checked for arguments in the execution and what to provide for those arguments.

My second idea was to add a GameObject field for the caster with the [SerializeReference] attribute to the action. The issue with this approach is the ability would need a way to reference the caster or at least have an empty field for it, and somehow provide that to the action all from the inspector. From my understanding, this isnā€™t possible and it sounds like a context class is supposed to be the solution to this problem.

You previously mentioned using a context class and dictionary to handle this type of situation, and while I feel I understand the concept of what a context class does, I do not understand how to implement it.

Does every ability require its own instance of the context class? How could I handle this? Given the example class you provided in an earlier reply, how could I implement it?

I apologize if it feels like I am treating you like Google, but even after researching Iā€™m struggling to find information on how to implement this concept, especially in this scenario. As always, thank you for the assistance.

Firstly, donā€™t use [SerializeReference] on those game object and Vector3 fields. Itā€™s only intended to be used on non-Unity object reference types. Aka plain C# objects. Just use [SerializeField] as normal on fields like those.

But the idea with the context object was to be passed through each action during the execution of each of them.

Basically:

public interface IAbilityAction
{
    System.Threading.Tasks RunActionAsync(AbilityActivationContext context, CancellationToken ct);
}

// somewhere else
private List<IAbilityAction> _actions = new();

public async Task RunActionsAsync(AbilityActivationContext context, CancellationToken ct)
{
    foreach (var action in _actions)
    {
        await action.RunActionAsync(context, ct);
    }
}

Part of it will involve gathering up info such as targets beforehand, and other data can be added to the context during execution.

Thank you again. I canā€™t believe I somehow didnā€™t understand the usage despite using the new input system and always adding the context argument my entire time with unity. Must be the months away from programming this semester.

Moving forward, Iā€™m thinking of how I should handle data between actions. For example, now that my projectile spawns, I want to have another action that moves it to a position. Using a dictionary seems like the right path but how can I ensure every object stored in it can easily be found by searching only the dictionary? What kind of key would work best?

Performing a lookup by type wouldnā€™t be ideal because an ability that creates two projectiles would use the same type. My next best guess is to use InstanceID or a custom incrementing ID in the context class as a key.

After some brainstorming I had a realization. I think I am unknowingly trying to reinvent the wheel. Instead of struggling with all of this, why not make custom visual scripting nodes and use that instead? It already has a blackboard and can sequence execution. Iā€™ve never used Unityā€™s visual scripting but I spent a decent amount of time with blueprints in Unreal 4.

Because you are so knowledgeable, I would like to know your opinion. Should I continue developing this system or use visual scripting with custom nodes? My biggest worry is possible bloat or networking limitations.

Iā€™ve never used visual scripting so I canā€™t comment. Far as Iā€™m aware its not the most performant and hasnā€™t really seen any development for a long time.

But in principle a node-based workflow is a good idea, and can be a lot more designer friendly. Though rather than the visual scripting package, I might suggest looking for a tool that lets you develop your own node graphs. xNode is the only one I really know, but I believe thereā€™s others out there.

But I donā€™t know how these might work or not with any networking.

Alright thank you.
I will research visual scripting further. If itā€™s possible to create nodes that function over time/asynchronously and thereā€™s not any other big limitations, ill pivot to using that. Otherwise, ill look into how to properly make a blackboard for the current system.