- Home /
Ability System with Modular Targeting: having trouble finding a good way to map between input components and effect components
I'm trying to make an ability system with modular targeting/inputs for a turn-based RPG game (using Unity)
What I mean by that is that most ability systems I've found use an inheritance hierarchy where there are different sub-classes for each different type of targeting, e.g. SingleTargetAbility, PBAoEAbility, VectorAbility, etc.
However it seems like this structure lacks flexibility or results in a huge, unwieldy number of subclasses. For example, say you made single target abilities, like Holy Smite and vector (choose a direction radiating out from your character) abilities, like Flame Lance. Now let's say you want to make a Push ability, where the player selects a unit, the selects a direction radiating out from that unit in which to push it. With the inheritance hierarchy method, it seems like you'd have to make a new class, SingleTargetThenVector ability to fit this use case. And so forth for any other combination of inputs.
Instead, I tried to make a system where an ability can have any number of player inputs (i.e. targeting, but also including stuff like "choose one of 3 options"), and any number of effects (like damage, push, heal, etc), and then the inputs are mapped to each effect.
This is what I came up with (abbreviated to include only the important parts):
public class AbilityInfo : ScriptableObject
{
public string abilityName = "New Ability";
[SerializeField]
public string id = System.Guid.NewGuid().ToString();
public List<AbilityInputInfo> inputInfos;
public List<AbilityEffect> abilityEffects;
}
public abstract class AbilityInputInfo
{
public abstract string uiPrefabName { get; set; }
public abstract void promptInput(Ability ability);
}
public enum AbilityInputSource : int
{
Caster = -1,
First = 0,
Second = 1,
Third = 2
}
public class AbilityEffect
{
public List<AbilityInputSource> playerInputSources;
public EffectInfo info;
}
public class Ability : MonoBehaviour
{
public GameObject caster { get { return gameObject; } private set { } }
public AbilityInfo info { get; private set; }
private List<System.Object> _inputs;
public static void Create(GameObject unit, AbilityInfo info)
{
Ability ability = unit.AddComponent<Ability>();
ability.info = info;
}
public void execute()
{
if (info.inputInfos.Count > 0)
{
_inputs.Clear();
Events.OnAbilityInput += onPlayerInput;
info.inputInfos[0].promptInput(this);
}
}
public void onPlayerInput(object input)
{
_inputs.Add(input);
if (_inputs.Count == info.inputInfos.Count)
{
Events.OnAbilityInput -= onPlayerInput;
executeEffects();
}
else if (_inputs.Count > info.inputInfos.Count)
{
Debug.LogError($"Too many inputs for {info.abilityName}: expected {info.inputInfos.Count}, got {_inputs.Count}");
}
else
{
info.inputInfos[_inputs.Count].promptInput(this);
}
}
private void executeEffects()
{
foreach (AbilityEffect abilityEffect in info.abilityEffects)
{
List<System.Object> effectInputs = new List<object>();
foreach (int inputSource in abilityEffect.playerInputSources)
{
if (inputSource == (int)AbilityInputSource.Caster)
{
effectInputs.Add(caster);
}
else
{
effectInputs.Add(_inputs[inputSource]);
}
}
abilityEffect.info.execute(effectInputs);
}
}
}
public abstract class EffectInfo
{
public void execute(List<object> inputs)
{
//Some shared logic
_execute(inputs);
}
protected abstract void _execute(List<object> inputs);
}
public class MoveEffectInfo : EffectInfo
{
protected override void _execute(List<object> inputs)
{
GameObject unit = (GameObject)inputs[0];
Vector3Int targetPos = (Vector3Int)inputs[1];
Movement movement = unit.GetComponent<Movement>();
movement.move(targetPos);
}
}
public class GroundSelectInputInfo : AbilityInputInfo
{
public override string uiPrefabName { get { return "GroundSelectInputUi"; } set { } }
[SerializeField]
int range = 5;
public override void promptInput(Ability ability)
{
GameObject gameObject = Utility.InstantiatePrefab(uiPrefabName, GameObject.FindGameObjectWithTag("Canvas"));
GroundSelectInputUi ui = gameObject.GetComponent<GroundSelectInputUi>();
ui.initialize(ability.caster.position, range);
}
}
There are a few issues with this though:
The mapping from input to effect is not type-safe.
Because there are many different types of inputs to effects (units, positions, etc.), I just use "object" as a catch-all and then plugging those into the execute of each effect, and then inside the effects casting to the correct type. However, if I input the mapping wrong in the editor (put 2 instead of 1), then it will be an execution error.
Distinguishing between dynamic player inputs and static inputs (like "caster" or "caster's position") is really janky.
I do it through an enum that represents both static inputs and indices to the dynamic inputs, but that seems really janky. For example, right now the indices only go up to three. I'd have to add FOUR, FIVE, SIX, etc. as I have abilities with more inputs.
Passing the inputs back to the ability is done through a brittle event system.
Because the input is done through Unity UI, I use an asynchronous callback event system and register a listener in the Ability object. However, the same listener is used for all AbilityInputs, so it's possible that the Ability could receive input from a different UI than it expected (if the UI had a bug and sent multiple onPlayerInput events, for example), and the system wouldn't recognize that.
I can't seem to find a way around these problems, but it seems like there must be a way to make an ability system with modular targeting, although I haven't seen any examples online.
Anyone know how these issues can be solved?