This document is about: QUANTUM 2
SWITCH TO

Effects

Overview

The EffectsSystem provides a way to execute custom behavior relevant to a specific entity (usually an Actor). Each effect is its own entity and can control its own lifetime; it can essentially life forever or until its owning entity is destroyed. Effects can be added and removed at runtime. Since effects are entities, the same entity prototype can be spawned multiple times and added/staked at will.

The EffectController lies at the core of the execution flow for all effects. To create a custom effect, simply have the effect's controller inherit from the EffectController and implement its API.

effects
Effects Execution Flow and API.

Example Implementation

This section exemplifies the EffectsSystem by presenting the HealthEffectController implementation included in the FPS Template.

C#

public unsafe class HealthEffectController : EffectController, ITimedEffectController
{
    //========== PUBLIC MEMBERS ===================================================================================

    public EHealthAction Action                  = EHealthAction.Add;
    public EHealthType   Type                    = EHealthType.Health;
    public FP            AbsoluteValuePerTick    = FP._10;
    public FP            PercentualValuePerTick  = FP._0;
    public FP            TickInterval            = FP._1;
    public int           TickCount               = 1;
    public bool          TickOnApply             = true;

    //========== EffectController INTERFACE =======================================================================

    internal override bool IsFinished(Frame frame, EntityRef entity)
    {
        // 1. The effect is active until all ticks are consumed
    
        HealthEffect* healthEffect = frame.GetComponent<HealthEffect>(entity);
        return healthEffect->TicksRemaining <= 0;
    }

    internal override bool Activate(Frame frame, EntityRef entity, EntityRef target)
    {
        if (TickCount <= 0)
        {
            throw new ArgumentException(nameof(TickCount));
        }

        Health* health = frame.TryGetComponent<Health>(target);
        if (health == null)
            return false;

        // 2. Add HealthEffect component to support custom rollbackable data, set initial values
    
        HealthEffect* healthEffect = frame.TryGetOrAddComponent<HealthEffect>(entity);

        healthEffect->TimeToNextTick = TickInterval;
        healthEffect->TicksRemaining = TickCount;

        if (TickOnApply == true)
        {
            Tick(frame, entity);
        }

        return true;
    }

    internal override void Update(Frame frame, EntityRef entity)
    {
        // 3. Ticking in intervals
        
        HealthEffect* healthEffect = frame.GetComponent<HealthEffect>(entity);

        healthEffect->TimeToNextTick -= frame.DeltaTime;

        if (healthEffect->TimeToNextTick <= FP._0)
        {
            Tick(frame, entity);

            healthEffect->TimeToNextTick += TickInterval;
        }
    }

    //========== ITimedEffectController INTERFACE =================================================================

    FP ITimedEffectController.GetTotalTime(Frame frame, EntityRef entity)
    {
        int ticks = TickOnApply == true ? TickCount : (TickCount - 1);
        return ticks * TickInterval;
    }

    FP ITimedEffectController.GetRemainingTime(Frame frame, EntityRef entity)
    {
        HealthEffect* healthEffect = frame.GetComponent<HealthEffect>(entity);
        return healthEffect->TimeToNextTick + (healthEffect->TicksRemaining - 1) * TickInterval;
    }

    //========== PRIVATE METHODS ==================================================================================

    private void Tick(Frame frame, EntityRef entity)
    {
        EntityRef owner  = frame.GetOwnerEntity(entity);
        Health*   health = frame.TryGetComponent<Health>(owner);
        
        // 4. Process only on entities which are alive

        if (health == null || health->IsAlive == false)
            return;

        // 5. Calcualte health amount
        
        FP desiredAmount = AbsoluteValuePerTick;

        if (PercentualValuePerTick > FP._0)
        {
            if (Type == EHealthType.Health || Type == EHealthType.HealthAndShield)
            {
                desiredAmount += health->MaxHealth * PercentualValuePerTick * FP._0_01;
            }
            else if (Type == EHealthType.Shield)
            {
                desiredAmount += health->MaxShield * PercentualValuePerTick * FP._0_01;
            }
        }

        if (desiredAmount <= FP._0)
            return;

        Effect* effect = frame.GetComponent<Effect>(entity);

        // 6. Prepare hit and health data
        
        HitData hitData = new HitData()
        {
            Target       = owner,
            Source       = effect->Source,
            Instigator   = frame.TryGetOwnerEntity(effect->Source),
            Position     = frame.GetComponent<Transform3D>(owner)->Position,
            BodyPartType = EBodyPartType.None,
            HitSource    = EHitSource.Effect,
        };

        HealthData healthData = new HealthData
        {
            Action        = Action,
            DesiredAmount = desiredAmount,
            Target        = hitData.Target,
            Source        = hitData.Source,
            Instigator    = hitData.Instigator,
            Type          = Type,
        };

        // 7. Invoke hit and decrease remaining tick count
        
        frame.Signals.Hit(hitData, healthData);

        if (TickCount > 0)
        {
            HealthEffect* healthEffect = frame.GetComponent<HealthEffect>(entity);
            healthEffect->TicksRemaining -= 1;
        }
    }
}
healing over time effect
Healing over time effect.

Creating a New Effect

This explains the step-by-step procedure to create a new Effect in the FPS Template.

  1. In the Quantum solution, create a new MyEffectController which inherits from EffectController.
  2. Add serializable fields to the controller (configuration).
  3. If needed, create a MyEffect.qtn which contains definition for the MyEffect component and other data structures such as roll-backable data.
  4. Override the base properties if needed and implement Activate(), Deactivate(), Update() and IsFinished() in the controller.
  5. Implement the available controller interfaces (e.g. IAttributesProvider, ...).
  6. Recompile the quantum.code solution.
  7. In Unity, create an empty GameObject and add the visual components / model as a child object.
  8. Add the Entity, EntityPrototype, EntityComponentController and EntityComponentEffect scripts to the GameObject.
  9. On the Entity component, set:
    • Transform Synchronization to Simple or Error Correction
    • Require Owner to true - the effect cannot exist without the owning entity
  10. On the EntityComponentController component, select MyEffectController from the EffectController drown down menu and fill defined properties.
  11. On the EntityComponentEffect component, fill out the defined properties.
  12. Add other entity components to define the behavior of the entity (EntityComponentMyEffect, ...)
  13. Create a prefab of the entity.
  14. Execute the asset resource generation via the Quantum > Generate Asset Resources menu.
  15. The ability is now ready to use - reference entity prototype.
Back to top