SPAM Framework Documentation

Examples

Jump to:

These are examples of use-cases that are supported in SPAM, and how you would implement them. Note that they are not exclusive to each other, but can be combined for a very intricate ability system. Feel free to reach out on email or ask in discord if you need help with behaviour for your game!

Area of effect ability that damages enemies and heals friends

Version required:

Start with deciding how you want to separate teams: by layers, tags or a property on a component.
Regardless of which route you take, create a two custom effects: One for healing and one for Damage. In these effects, only apply them to the target if it's on the correct team.

This is the general outline for both effects:

public class DamageEnemyEffectSO : AbilityEffectSO  
{  
    [SerializeField] private int _damage; // Or healing value 

    // Omitted for brevity.
    protected override string _metaHelpDescription => $"";  

    public override void ApplyTo(IAbilityTarget target, Vector3 abilityPos, IAbilityData ability, AbilityInvoker invoker)  
    {  
        // Get the team of the caster. 
        // Here all three different ways to get it are shown
        // TeamComponent is a component that you've created for your game
        // that holds the team name of the character.
        var casterTeam = invoker.GetComponent<TeamComponent>().TeamName;
        casterTeam = invoker.gameObject.layer;
        casterTeam = invoker.gameObject.tag;

        var targetTeam = target.Transform.GetComponent<TeamComponent>().TeamName;
        targetTeam = target.Transform.gameObject.layer;
        targetTeam = target.Transform.gameObject.tag;

        var sameTeam = casterTeam == targetTeam;
        // Invert this (!sameTeam) if you want it to only apply to the same team
        if(sameTeam) return;

        // Here the pre-supplied SPAM component is used, but you could as well
        // use any of your components that hold health or can take damage.
        var damageable = target.Transform.GetComponent<IDamageable>();  
        damageable.Damage(_damageValue);  
    }  
}

Non-linear ability upgrades (talent tree, skill tree...)

Version required:

This can be achieved by thinking of ranks as variants.

Rank 1 is the base ability. If the player can choose between two different upgrades for the ability, create a rank 2 and 3, and when the player selects a specific rank you call _ability.SetRank(rankNumer) instead of _ability.IncreaseRank().

Repeat this for any subsequent upgrades.

If you need more assistance with different solutions for this, don't hesitate to reach out.

Vampiric Damage (heal for X% damage dealt)

Version required:

Like many other use cases this one is a custom effect. In its simplest form, all you need is a effect that has a damage value and a percentage healed value. 

public class VampiricDamageEffectSO : AbilityEffectSO  
{  
    [SerializeField] private int _damage;
    [SerializeField] private float _healPercent; 

    // Omitted for brevity.
    protected override string _metaHelpDescription => $"";  

    public override void ApplyTo(IAbilityTarget target, Vector3 abilityPos, IAbilityData ability, AbilityInvoker invoker)  
    {  
        // SPAMs built-in components used, you can use your own.
        var damageable = target.Transform.GetComponent<IDamageable>();  
        damageable.Damage(_damage);

        var healable = invoker.GetComponent<IHealable>();
        healable.Heal(_damage * _healPercent)
    }  
}

If you have a more intricate health and stats system in place, maybe the actual damage you deal is reduced by the target's armor and resistances. In that case you could just return the actual damage dealt from the component that is responsible for taking damage, and heal the caster a percentage of that. The implementation of ApplyTo() would in that case be:

var damageable = target.Transform.GetComponent<YourDamageableComponent>();  
var actualDamage = damageable.Damage(_damage);

var healable = invoker.GetComponent<IHealable>();
healable.Heal(actualDamage * _healPercent)

Scaling Damage and/or healing by stats

Version required:

Prerequisites:

In the simplest form, all that's required is an effect that reads the involved stat(s) and calculates the damage before it's applied to the target.

Say your stat system has an enum which holds all possible stat types (attackpower, strength...), then the effect could be implemented like this:

public class StatScaledDamageEffectSO : AbilityEffectSO  
{  
    // This exposes the stat to use, and base damage, in the effect settings
    [SerializeField] private StatsEnum _damageModifierStat;  
    [SerializeField] private int _baseDamage;  

    protected override string _metaHelpDescription => "";  

    public override void ApplyTo(IAbilityTarget target, Vector3 abilityPos, IAbilityData ability,  
    AbilityInvoker invoker)  
    {  
        // Get the targets component that's responsible for taking damage  
        // Here SPAMs built in interface is used.  
        var damageable = target.Transform.GetComponent<IDamageable>();  
        if (damageable == null) return; // Can't deal damage if target isn't damageable  

        // Get the component that holds the stats for the character  
        var casterStats = invoker.GetComponent<CharacterStats>();  

        // Get the actual stat to scale from  
        var damageModifierStat = casterStats.GetStatValue(_damageModifierStat);  

        // The actual calculation for how the stat affects the damage is  
        // highly specific to your game. This is just a very simple example so just  
        // add the stat to the final damage.  
        var finalDamage = _baseDamage + damageModifierStat;  

        damageable.Damage(finalDamage);  
    }  
}

Of course this implementation will vary alot depending on your actual stat system, but the general approach to implementing scaling damage will be similar to the above implementation.

Make lightning abilities do more damage when it rains.

Version required:

For this you have multiple options if you want to let SPAM handle this, and which path you choose depends on what other systems you have in place. Note that this example is applicable any time you want an ability to change it effects depending on the state of the target or the world. It could be applied for races (orc, elf, human), states (stunned, slowed), status effects (on fire) or anything that you could use a condition for.

If you already have the systems in place for checking if its raining when lightning damage is applied, the most simple strategy is to create a custom damage effect that you add the damage type to. Then you pass this damage to your health system which checks if its raining and then calculates the final damage. Here's a basic example of how that could be achieved:

public class ElementalDamageEffectSO : AbilityEffectSO  
{  
    // fire, wind, lightning...  
    [SerializeField] private ElementEnum _damageElement; 
    [SerializeField] private int _baseDamage;  

    protected override string _metaHelpDescription => "";  

    public override void ApplyTo(IAbilityTarget target, Vector3 abilityPos, IAbilityData ability,  
    AbilityInvoker invoker)  
    {  
        // Get the component that's responsible for taking damage.  
        // Note that here SPAMs built-in interfaces aren't used.  
        // You would get your own component here  
        var targetHealthComponent = target.Transform.GetComponent<HealthComponent>(); 

        if (targetHealthComponent is null) return; // Can't deal damage if target isn't damageable  

        // Pass the element and the damage to the health component.  
        // It will be responsible for checking if the target is under any  
        // condition that will increase the damage for this element.  
        targetHealthComponent.DealDamage(_baseDamage, _damageElement);  
    }  
}

If you have none of these systems in place, SPAM has support for this out of the box.
You can add this either as a conditional effect or a custom damage effect.

Both options utilise the powerful Conditions System of SPAM, and have their own pros and cons. The conditional effect route is more versatile and most work is done in the editor, which makes it effortless to change and modify. The custom damage is effect is more code-oritened.
However, note that you could combine the two routes for a very intricate damage system!

Create Condition
Regardless of route you would start with creating a Condition: Wet. This is the condition that you will add to characters when it rains or some other trigger that you have in your game (standing in a puddle, getting hit by a water ability...)

img/example-wet-conditionpng

Add condition to target
There are built-in effects in SPAM that lets abilities add conditions out-of-the box! See Adding and removing conditions for more info.

This example uses a component called ConditionZone that adds a given condition to the target when it enters/exits it. It's attached to a GameObject with a collider set to trigger. Note that you can add the condition in any way you wish by calling the same methods on the Ability Conditions Target.

public class ConditionZone : MonoBehaviour  
{  
    [SerializeField] private AbilityConditionSO _conditionToApply;  

    private void OnTriggerEnter(Collider other)  
    {  
        // This component is built into SPAM  
        var conditionsTarget = other.GetComponent<AbilityConditionsTarget>();  
        if (!conditionsTarget) return;  

        // Lifetime of 0 means the condition will be permanent until removed,  
        // and it's ok to send null as caster as this condition has no secondary effects.  
        conditionsTarget.AddCondition(_conditionToApply, 0, null);  
    }  

    private void OnTriggerExit(Collider other)  
    {  
        // Do the same as on enter, but remove component instead.  
        var conditionsTarget = other.GetComponent<AbilityConditionsTarget>();  
        if (!conditionsTarget) return;  

        conditionsTarget.RemoveCondition(_conditionToApply);  
    }  
}

With this in place, you can now chose wether to go the conditional effect or custom damage effect route.

Conditional Effect Route
Create a conditional effect: Extra Damage When Wet, and give it a damage effect to apply.

img/ce-wet-damagepng

Now you can add this conditional effect to all abilities which deal lightning damage (or should so extra damage to wet targets). It's located under Ability effects.

img/wet-ce-on-abilitypng

Custom Damage Effect Route
This route requires more code but is still very powerful.
It moves the logic away from SPAMs editors and into the code, which has both pros and cons depending on what you're most comfortable with.

Start by creating a custom effect that derives from AbilityEffectSO.
Add the following as serialized field (so you can set them in the editor):

Note that this effect can now check for any condition(s) and apply extra damage if the target has any (or all, you decide!) of the conditions.

In the Apply function, get the target's Ability Conditions Target-component, and check if it has the applicable Condition(s) before applying damage. 

public class ConditionDamageEffectSO : AbilityEffectSO  
{  
    [SerializeField] private int _baseDamage;  
    [SerializeField] private AbilityConditionSO _conditionForExtraDamage;  
    [SerializeField] private int _extraConditionDamage;  

    protected override string _metaHelpDescription = "";

    public override void ApplyTo(IAbilityTarget target, Vector3 abilityPos, IAbilityData ability, AbilityInvoker invoker)  
    {  
        // SPAM's build in interface is used here. It could be any component  
        // that can receive damage.  
        var damageable = target.Transform.GetComponent<IDamageable>();  
        if (damageable is null) return;  

        var conditionsTarget = target.Transform.GetComponent<AbilityConditionsTarget>();  
        if (!conditionsTarget) return;  

        var targetHasRequiredCondition = conditionsTarget.HasCondition(_conditionForExtraDamage);  
        // If target has the condition, add the extra damage, else add nothing  
        var additionalDamageFromCondition = targetHasRequiredCondition ? _extraConditionDamage : 0;  

        var finalDamage = _baseDamage + additionalDamageFromCondition;  

        damageable.Damage(finalDamage);  
    }  
}

Stun a character (with and without conditions)

Version required:

The actual game-mechanic of stunning a character varies widly from game to game and hence is left out of this example. This examples handles "How you apply stun to a character", and not "What happens when a character is stunned".

The most simple implementation of this will be if you already have a system that can handle "stunning" characters. If you Character Controller has support for this, all you would need is a simple component and the built-in stun effect.

Only Effect (any version)
Create an stun effect (included in SPAM under Create new effect). This effect looks for an IStunable on the target when applied (you can check the code in StunEffectSO.cs). Assign this effect to an Ability that should stun its target.

Since it's preferable in Unity to have small components that only do one thing, create a Stunable component which you can add to any GameObject that has a Character Controller and should be stunable. Note that this example takes for granted that your Character Controller has a method or property that allows/disallows movement.

public class Stunable : MonoBehaviour, IStunable  
{  
    // Reference to your Character Controller  
    [SerializeField] private CharacterController _controller;  

    private float _stunTime;  
    public bool IsStunned => 0 < _stunTime;  

    // This is called automatically by SPAM's "AddStunEffect"  
    // when a stun effect is applied to a target with this component.  
    public void Stun(float stuntime)  
    {  
        this._stunTime = stuntime;  
        this.enabled = true;  
        // This is where you tell your CC to stop  
        // It could be a method, property or similar.  
        _controller.MovementEnabled = false;  
    }  

    // This will only run when this.enabled = true.  
    // Doing this helps with performance as it will not be called  
    // when it's not doing anything.  
    private void Update()  
    {  
        _stunTime -= Time.deltaTime;  
        if (_stunTime <= 0)  
        {  
            // The target has now been stunned for the given time,
            // allow movement again  
            _controller.MovementEnabled = true;  
            this.enabled = false;  
        }  
    }  
}

Now all you need to do it cast the ability on a valid target, and it will be stunned for as long as you set the stun time to in the effect.

Using Conditions (Not available in lite, requires version 1.2.0+)
See also: Conditions

This route is a little more flexible and versatile but requires a little more advanced code to set up. It enables you react to changes in the stun-status, f.eg. it can handle when stun condition was "dispelled" (removed) instead of only when it ran for its full time (expired). In other terms, it makes the code follow an event-driven architecture, which is a very powerful pattern in game development.
You could utilize this to make highly generic components that allows you to react to changes in a targets conditions in specific ways while also being able to set this behaviour in the editor (that kind of component/behaviour is out of scope for this example, but feel free to ask in Discord if you would like to know more).

Create a Stun condition, with no special settings. This will be used as a "Marker-condition" (aka event-data).

img/stunned-conditionpng

The Ability Conditions Target-component exposes the events that you can use to react on changes in a target's conditions.

You can listen to these events in a MonoBehaviour:

public class Stunable : MonoBehaviour  
{  
    // This reference is required to listen to events  
    [SerializeField] private AbilityConditionsTarget _conditionsTarget;  

    // Assign your stun condition here  
    [SerializeField] private AbilityConditionSO _stunCondition;  

    // This is a public property so other scripts can read it  
    public bool IsStunned { get; private set; }  

    // We can also expose our own event for a more event-driven architecture.  
    // Other components can listen for this event.  
    public event Action<bool> StunnedChanged;  

    private void Awake()  
    {  
        _conditionsTarget.ConditionAdded += OnConditionAdded;  
        // A condition can end in both and expire- removed event, so both  
        // needs to be subscribed to since we don't care how it was removed.  
        _conditionsTarget.ConditionRemoved += OnConditionRemoved;  
        _conditionsTarget.ConditionExpired += OnConditionRemoved;  

        // The component also exposes the following events:  
        // _conditionsTarget.ConditionExtended;  
        // _conditionsTarget.ConditionTicked;  
    }  

    private void OnConditionAdded(AbilityConditionSO addedCondition, float _)  
    {  
        // If it's not the condition we're listening for, don't do anything.  
        if (!addedCondition.IsSameAs(_stunCondition)) return;  

        IsStunned = true;  
        StunnedChanged?.Invoke(true);  
    }  

    private void OnConditionRemoved(AbilityConditionSO removedCondition)  
    {  
        // If it's not the condition we're listening for, don't do anything.  
        if (!removedCondition.IsSameAs(_stunCondition)) return;  

        IsStunned = false;  
        StunnedChanged?.Invoke(false);  
    }  

    private void OnDestroy()  
    {  
        // Always remember to unregister from events when the component is destroyed,  
        // else you'll have null reference exceptions down the road.  
        _conditionsTarget.ConditionAdded -= OnConditionAdded;  
        _conditionsTarget.ConditionRemoved -= OnConditionRemoved;  
        _conditionsTarget.ConditionExpired -= OnConditionRemoved;  
    }  
}

With this component in place, you can choose wether you want to listen to its events or read its IsStunned property to stop your character from moving.

Backlinks: