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.
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;
}
}
}
Creating a New Effect
This explains the step-by-step procedure to create a new Effect in the FPS Template.
- In the Quantum solution, create a new
MyEffectController
which inherits fromEffectController
. - Add serializable fields to the controller (configuration).
- If needed, create a
MyEffect.qtn
which contains definition for theMyEffect
component and other data structures such as roll-backable data. - Override the base properties if needed and implement
Activate()
,Deactivate()
,Update()
andIsFinished()
in the controller. - Implement the available controller interfaces (e.g.
IAttributesProvider
, ...). - Recompile the
quantum.code
solution. - In Unity, create an empty GameObject and add the visual components / model as a child object.
- Add the
Entity
,EntityPrototype
,EntityComponentController
andEntityComponentEffect
scripts to the GameObject. - On the
Entity
component, set:Transform Synchronization
toSimple
orError Correction
Require Owner
totrue
- the effect cannot exist without the owning entity
- On the
EntityComponentController
component, selectMyEffectController
from theEffectController
drown down menu and fill defined properties. - On the
EntityComponentEffect
component, fill out the defined properties. - Add other entity components to define the behavior of the entity (
EntityComponentMyEffect
, ...) - Create a prefab of the entity.
- Execute the asset resource generation via the
Quantum > Generate Asset Resources
menu. - The ability is now ready to use - reference entity prototype.