Effect Components in Forge allows developers to extend effect functionality through a modular, composable approach. Components can add custom behaviors, validation logic, and react to different events in an effect's lifecycle.
For a practical guide on using components, see the Quick Start Guide.
Components follow the composition pattern, allowing you to build complex effect behaviors without inheritance. Each component implements the IEffectComponent interface and can be attached to any EffectData.
public readonly struct EffectData(
// Other parameters...
IEffectComponent[]? effectComponents = null)
{
// Implementation...
public IEffectComponent[]? EffectComponents { get; }
}To create a custom component, implement the IEffectComponent interface:
public interface IEffectComponent
{
IEffectComponent CreateInstance();
bool CanApplyEffect(in IForgeEntity target, in Effect effect);
bool OnActiveEffectAdded(IForgeEntity target, in ActiveEffectEvaluatedData activeEffectEvaluatedData);
void OnPostActiveEffectAdded(IForgeEntity target, in ActiveEffectEvaluatedData activeEffectEvaluatedData);
void OnActiveEffectUnapplied(IForgeEntity target, in ActiveEffectEvaluatedData activeEffectEvaluatedData, bool removed);
void OnActiveEffectChanged(IForgeEntity target, in ActiveEffectEvaluatedData activeEffectEvaluatedData);
void OnEffectApplied(IForgeEntity target, in EffectEvaluatedData effectEvaluatedData);
void OnEffectExecuted(IForgeEntity target, in EffectEvaluatedData effectEvaluatedData);
}The interface provides default implementations for all methods, so you only need to override the ones relevant to your component's functionality.
CreateInstance is called when the effect is applied and allows the component to provide either a shared (stateless) instance or return a new instance for per-application (stateful) data.
Override this method when your component holds data that must be isolated per-effect application, such as event subscriptions or runtime counters.
public class ExampleComponent : IEffectComponent
{
private int _someState;
public IEffectComponent CreateInstance()
{
// Return a new instance so each effect application has its own state
return new ExampleComponent();
}
}Use cases:
- Tracking data or resources that must not be shared across multiple effect instances.
- Managing event subscriptions or references tied to a specific application of an effect.
- Ensuring thread safety or isolation when effects are applied to different targets simultaneously.
Called during the validation phase to determine if an effect can be applied. Return false to block the application.
public bool CanApplyEffect(in IForgeEntity target, in Effect effect)
{
// Custom validation logic
return true; // Allow application by default
}Use cases:
- Checking if target meets requirements.
- Implementing application chances.
- Restricting effects based on game state.
Called when a non-instant effect is added to a target. Return false to inhibit the effect.
public bool OnActiveEffectAdded(IForgeEntity target, in ActiveEffectEvaluatedData activeEffectEvaluatedData)
{
// Custom initialization logic
return true; // Keep the effect active by default
}Use cases:
- Adding temporary tags or flags.
- Setting up event subscriptions.
- Initializing effect-specific game state.
OnPostActiveEffectAdded is called after all components’ OnActiveEffectAdded callbacks have completed, and the effect has finished its initial application logic. At this point, the effect is fully initialized.
Override this method to perform actions that rely on other components being initialized, or when you need to trigger behaviors that should occur after the effect is completely active.
public void OnPostActiveEffectAdded(IForgeEntity target, in ActiveEffectEvaluatedData activeEffectEvaluatedData)
{
// Logic here runs after all initialization and validation is complete
// For example, attempt activation if not inhibited
if (!activeEffectEvaluatedData.ActiveEffectHandle.IsInhibited)
{
// Custom post-activation logic
}
}Use cases:
- Conditionally activating abilities granted by earlier components.
- Synchronizing with other components after full effect application.
- Triggering animations, particles, or gameplay effects that should be delayed until the effect is stable.
Called when an effect is unapplied or a stack is removed.
public void OnActiveEffectUnapplied(IForgeEntity target, in ActiveEffectEvaluatedData activeEffectEvaluatedData, bool removed)
{
// Custom cleanup logic
if (removed) {
// Effect was completely removed
} else {
// Just a stack was removed
}
}Use cases:
- Removing temporary tags or flags.
- Cleaning up game state.
- Removing event subscriptions.
Called when an effect changes. This occurs specifically when:
- The effect level changes.
- Modifier values are updated.
- Stack count changes.
- Inhibition state changes.
public void OnActiveEffectChanged(IForgeEntity target, in ActiveEffectEvaluatedData activeEffectEvaluatedData)
{
// React to effect changes
}Use cases:
- Updating related game systems.
- Adjusting dependent mechanics.
Called for all effects when applied, including instant effects and stack applications.
public void OnEffectApplied(IForgeEntity target, in EffectEvaluatedData effectEvaluatedData)
{
// React to effect application
}Use cases:
- Triggering reactions to both instant and duration effects.
- Cross-effect interactions.
Called when an instant or periodic effect executes its modifiers.
public void OnEffectExecuted(IForgeEntity target, in EffectEvaluatedData effectEvaluatedData)
{
// React to effect execution
}Use cases:
- Adding secondary effects based on execution results.
- Tracking execution statistics.
- Triggering additional gameplay reactions.
Example custom component:
// Component that tracks damage thresholds and applies additional effects
public class DamageThresholdComponent : IEffectComponent
{
private readonly float _threshold;
private readonly Effect _thresholdEffect;
private float _accumulatedDamage;
private EventSubscriptionToken? _damageEventToken;
public DamageThresholdComponent(float threshold, Effect thresholdEffect)
{
_threshold = threshold;
_thresholdEffect = thresholdEffect;
}
// Guarantees each effect application has its own unique instance and state
public IEffectComponent CreateInstance()
{
return new DamageThresholdComponent(_threshold, _thresholdEffect);
}
public bool OnActiveEffectAdded(IForgeEntity target, in ActiveEffectEvaluatedData activeEffectEvaluatedData)
{
_accumulatedDamage = 0f;
// Subscribe to an "events.combat.damage_taken" event using Forge's Events system
var damageTakenTag = Tag.RequestTag(target.TagsManager, "events.combat.damage_taken");
_damageEventToken = target.Events.Subscribe(damageTakenTag, data =>
{
_accumulatedDamage += data.EventMagnitude;
if (_accumulatedDamage >= _threshold)
{
_accumulatedDamage = 0;
target.EffectsManager.ApplyEffect(_thresholdEffect);
}
});
return true;
}
public void OnActiveEffectUnapplied(
IForgeEntity target,
in ActiveEffectEvaluatedData activeEffectEvaluatedData,
bool removed)
{
if (removed && _damageEventToken is not null)
{
target.Events.Unsubscribe(_damageEventToken.Value);
_damageEventToken = null;
}
}
}When you apply a duration (non-instant) effect, you receive an ActiveEffectHandle from the EffectsManager. This handle provides access to the specific component instances that were created for this effect application.
This is useful if you need to check runtime state, interact with a component that manages resources, or access data (such as granted abilities or custom counters) unique to this particular effect instance.
You can retrieve a component instance of a given type using the handle's generic GetComponent<T>() method:
// Apply an effect and get the handle.
ActiveEffectHandle? handle = entity.EffectsManager.ApplyEffect(new Effect(effectData, ownership));
if (handle is not null)
{
// Retrieve a specific component instance used by this effect.
var grantAbilityComponent = handle.GetComponent<GrantAbilityEffectComponent>();
if (grantAbilityComponent is not null)
{
// Access runtime data exposed by the component
IReadOnlyList<AbilityHandle> grantedAbilities = grantAbilityComponent.GrantedAbilities;
// ... use grantedAbilities as needed
}
// You can also enumerate all component instances for additional logic
foreach (var component in handle.ComponentInstances)
{
// Inspect or interact with component instances
}
}GetComponent<T>()returns the first component instance of typeT(ornullif none exists).ComponentInstancesexposes all component instances for this effect (may hold per-instance state).
Typical use cases:
- Accessing granted ability handles from a
GrantAbilityEffectComponent. - Inspecting or updating internal state on a custom component.
- Coordinating follow-up logic or queries in gameplay systems.
For more details on the structure of ActiveEffectHandle, see the ActiveEffectHandle documentation.
Components can be used to implement complex systems that integrate with your game's mechanics:
- Combat Reaction System: Components that trigger reactions between elements.
- Cooldown Management: Components that track and enforce cooldowns between effect applications.
- Cross-Effect Coordination: Components that coordinate between multiple active effects.
- Attribute Threshold Monitoring: Components that trigger effects when attributes cross thresholds.
- AI Behavior Modification: Components that adjust AI behavior when effects are active.
Forge includes several built-in components that demonstrate the component system's capabilities and provide ready-to-use functionality.
Grants one or more abilities to the target entity. This is the primary bridge between the Effects system and the Abilities system.
public class GrantAbilityEffectComponent(GrantAbilityConfig[] grantAbilityConfigs) : IEffectComponent
{
public IReadOnlyList<AbilityHandle> GrantedAbilities { get; }
// Implementation...
}var grantConfig = new GrantAbilityConfig(
AbilityData: fireballData,
ScalableLevel: new ScalableInt(1), // Scales with effect level if a curve is defined
RemovalPolicy: AbilityDeactivationPolicy.CancelImmediately, // Cancels running instances immediately when effect ends
InhibitionPolicy: AbilityDeactivationPolicy.CancelImmediately, // Cancels running instances immediately if effect is inhibited
TryActivateOnGrant: false, // Do not try to activate automatically when granted
TryActivateOnEnable: false, // Do not try to activate automatically when enabled back from inhibition
LevelOverridePolicy: LevelComparison.Higher // Update level if higher than existing grant
);
// Keep a reference to the component if you need to access the granted ability handles later
var grantComponent = new GrantAbilityEffectComponent([grantConfig]);
var grantEffect = new EffectData(
"Grant Fireball",
new DurationData(DurationType.Infinite),
effectComponents: [grantComponent]
);
// Apply the effect
entity.EffectsManager.ApplyEffect(new Effect(grantEffect, ownership));
// Access the handle directly from the component instance
AbilityHandle fireballHandle = grantComponent.GrantedAbilities[0];Key points:
- Direct Handle Access: You can keep a reference to the component instance to access its
GrantedAbilities, which contains theAbilityHandles created by this specific effect application. Alternatively, use the effect handle'sGetComponent<GrantAbilityEffectComponent>()method to retrieve the runtime component instance when needed. - Lifecycle Management: Automatically handles granting, removing, and inhibiting abilities based on the effect's lifecycle and the configured policies.
- Permanent vs. Temporary:
- If used in an Instant effect, the ability is granted permanently.
- If used in a Duration effect, the ability exists only while the effect is active (unless removal policy is set to
Ignore).
Adds a random chance for effects to be applied, with support for level-based scaling.
public class ChanceToApplyEffectComponent(IRandom randomProvider, ScalableFloat chance) : IEffectComponent
{
// Implementation...
}The ChanceToApplyEffectComponent uses the IRandom interface to generate random values for determining if an effect should be applied:
public interface IRandom
{
int NextInt();
int NextInt(int maxValue);
int NextInt(int minValue, int maxValue);
float NextSingle();
double NextDouble();
long NextInt64();
long NextInt64(long maxValue);
long NextInt64(long minValue, long maxValue);
void NextBytes(byte[] buffer);
void NextBytes(Span<byte> buffer);
}The component specifically uses the NextSingle() method, which returns a random floating-point number between 0.0 (inclusive) and 1.0 (exclusive). This allows for a consistent random number generation implementation that can be swapped or mocked for testing.
// Create a "Stun" effect with a 25% chance to apply
var stunEffectData = new EffectData(
"Stun",
new DurationData(
DurationType.HasDuration,
new ModifierMagnitude(
MagnitudeCalculationType.ScalableFloat,
new ScalableFloat(3.0f)
)
),
effectComponents: new[] {
new ChanceToApplyEffectComponent(
randomProvider, // Your game's random number generator
new ScalableFloat(0.25f) // 25% chance to apply
)
}
);Advanced usage with level scaling:
// Create a "Critical Hit" effect with a chance that scales with level
var criticalHitEffectData = new EffectData(
"Critical Hit",
new DurationData(DurationType.Instant),
[/*...*/],
effectComponents: new[] {
new ChanceToApplyEffectComponent(
randomProvider,
new ScalableFloat(
0.1f, // Base 10% chance
new Curve([
new CurveKey(1, 1.0f), // Level 1: 10%
new CurveKey(5, 2.0f), // Level 5: 20%
new CurveKey(10, 3.5f) // Level 10: 35%
])
)
)
}
);Key points:
- Uses the provided random provider for chance determination.
- Chance can scale with effect level using
ScalableFloat. - Validates during
CanApplyEffect, before any effect application logic.
Adds tags to the target entity while the effect is active. These tags are automatically removed when the effect ends. See the Tags documentation for more on tags.
public class ModifierTagsEffectComponent(TagContainer tagsToAdd) : IEffectComponent
{
// Implementation...
}Usage example:
// Create a "Burning" effect that adds the "Status.Burning" tag to the target
var burningEffectData = new EffectData(
"Burning",
new DurationData(
DurationType.HasDuration,
new ModifierMagnitude(
MagnitudeCalculationType.ScalableFloat,
new ScalableFloat(10.0f)
)
),
new[] {
new Modifier("CombatAttributeSet.CurrentHealth", ModifierOperation.Add, new ModifierMagnitude(MagnitudeCalculationType.ScalableFloat, new ScalableFloat(-5)))
},
periodicData: new PeriodicData(
period: new ScalableFloat(2.0f),
executeOnApplication: true,
periodInhibitionRemovedPolicy: PeriodInhibitionRemovedPolicy.ResetPeriod
),
effectComponents: new[] {
new ModifierTagsEffectComponent(
tagsManager.RequestTagContainer(new[] { "status.burning" })
)
}
);Key points:
- Only works with duration effects (not instant).
- Tags are automatically added when the effect is applied.
- Tags are automatically removed when the effect ends completely.
- With stacked effects, tags remain until all stacks are removed.
Validates if a target meets tag requirements for effect application and manages ongoing effect states based on tags.
public class TargetTagRequirementsEffectComponent(
TagRequirements applicationTagRequirements,
TagRequirements removalTagRequirements,
TagRequirements ongoingTagRequirements) : IEffectComponent
{
// Implementation...
}The TagRequirements struct is a powerful mechanism for evaluating tag conditions on entities, used by the TargetTagRequirementsEffectComponent.
public readonly struct TagRequirements(
TagContainer? requiredTags = null,
TagContainer? ignoreTags = null,
TagQuery? tagQuery = null)
{
// Implementation...
}- RequiredTags: Tags that must all be present on the target.
- IgnoreTags: Tags that must not be present on the target (any match will fail).
- TagQuery: A complex query expression for advanced tag matching.
public bool RequirementsMet(in TagContainer targetContainer)
{
var hasRequired = RequiredTags is null || targetContainer.HasAll(RequiredTags);
var hasIgnored = IgnoreTags is not null && targetContainer.HasAny(IgnoreTags);
var matchQuery = TagQuery is null || TagQuery.IsEmpty || TagQuery.Matches(targetContainer);
return hasRequired && !hasIgnored && matchQuery;
}For requirements to be met:
- Target must have ALL required tags.
- Target must have NONE of the ignore tags.
- Target must match the tag query (if one is provided).
Tag queries allow for more complex expressions than simple "has all" and "has none" logic. See the Tags documentation for more on tag queries.
// Create a query that matches if:
// (Target has EITHER "Fire" OR "Ice") AND (Target does NOT have both "Water" AND "Metal")
var query = new TagQuery();
query.Build(new TagQueryExpression(tagsManager)
.AllExpressionsMatch()
.AddExpression(new TagQueryExpression(tagsManager)
.AnyTagsMatch()
.AddTag("Fire")
.AddTag("Ice"))
.AddExpression(new TagQueryExpression(tagsManager)
.NoExpressionsMatch()
.AddExpression(new TagQueryExpression(tagsManager)
.AllTagsMatch()
.AddTag("Water")
.AddTag("Metal"))));// Create a "Frost" effect that only applies to targets with the "Wet" tag,
// is removed if target gains the "Fire" tag, and is inhibited if target has the "Cold.Immune" tag
var frostEffectData = new EffectData(
"Frost",
new DurationData(
DurationType.HasDuration,
new ModifierMagnitude(
MagnitudeCalculationType.ScalableFloat,
new ScalableFloat(8.0f)
)
),
[/*...*/],
effectComponents: new[] {
new TargetTagRequirementsEffectComponent(
// Application requirements: target must have "Wet" tag
applicationTagRequirements: new TagRequirements(
requiredTags: tagsManager.RequestTagContainer(new[] { "Wet" })
),
// Removal requirements: effect is removed if target gets "Fire" tag
removalTagRequirements: new TagRequirements(
tagQuery: new TagQuery(tagsManager, "Fire")
),
// Ongoing requirements: effect is inhibited if target has "Cold.Immune" tag
ongoingTagRequirements: new TagRequirements(
ignoreTags: tagsManager.RequestTagContainer(new[] { "Cold.Immune" })
)
)
}
);Key points:
- Dynamically monitors tag changes on the target.
- Can prevent application, force removal, or toggle inhibition.
- Automatically cleans up event subscriptions when the effect is removed.
- Uses
TagRequirementsto define complex tag conditions.
Components can be combined to create complex effect behaviors:
var complexEffectData = new EffectData(
"Complex Effect",
/* other parameters */
effectComponents: new IEffectComponent[] {
new ChanceToApplyEffectComponent(randomProvider, new ScalableFloat(0.5f)),
new TargetTagRequirementsEffectComponent(/* requirements */),
new ModifierTagsEffectComponent(/* tags to add */),
new CustomEffectComponent() // Your own custom component
}
);- Single Responsibility: Each component should handle one specific aspect of behavior.
- Manage Resources: Clean up any subscriptions or external resources in
OnActiveEffectUnapplied. - Consider Performance: Components are called frequently, so optimize for performance.
- Use Return Values Correctly: Return
falsefrom validation methods only when you want to block behavior. - Leverage Existing Components: Combine with built-in components when possible.
- Component Composition: Use multiple simple components instead of one complex component.
- Avoid Circular Dependencies: Be careful not to create recursive loops with components that apply effects.
- Error Handling: Components should be robust against unexpected states and not throw exceptions.
- Documentation: Document any requirements or assumptions your custom components make.
- Testing: Test components in isolation and in combination with other components.