Class BaseAction
Abstract base class for all ECA actions. Serialized inline via [SerializeReference] on Trigger assets. Derive from this class and add [Serializable] to create concrete actions.
[Serializable]
public abstract class BaseAction : IAction
- Inheritance
-
BaseAction
- Implements
- Derived
- Inherited Members
Fields
StopChainOnFailure
When true and this action implements IAbortableAction, returning
false from TryExecute(ICharacter, EventData) aborts the remainder of the
current action list (e.g. stop applying damage after a resource consume fails).
Has no effect on actions that do not implement IAbortableAction.
[Tooltip("If this action implements IAbortableAction, abort the rest of the action chain when TryExecute returns false.")]
public bool StopChainOnFailure
Field Value
TargetSelector
Optional selector that picks one or more targets for this action. When set, the action runs once per selected target. When unset, the action runs once against the current event data (reading TargetCharacter or falling back to the initiator).
[Tooltip("Optional selector for this action. When unset the action runs once against the current event target.")]
[SerializeReference]
public TargetSelector TargetSelector
Field Value
Methods
Execute(ICharacter, EventData)
Executes the action. Must be implemented by derived classes.
public abstract void Execute(ICharacter initiator, EventData eventData)
Parameters
initiatorICharacterThe character initiating the action.
eventDataEventDataEvent data for the action.
GetTooltipContribution()
Returns a short, designer-facing tooltip line describing this action's effect, or
null when the action has nothing to contribute. Override on actions that
produce a player-visible outcome (damage, heal, buff apply, knockback, etc.) so
ability tooltips can list effects without designer authoring text twice.
public virtual string GetTooltipContribution()
Returns
Remarks
The base implementation returns null. Aggregators
(BaseAbilityTemplate) skip null/whitespace contributions, so most
actions need not override.
IsClientPeer(ICharacter, EventData)
True when this peer has a screen — i.e. it is a client rather than the dedicated server.
protected static bool IsClientPeer(ICharacter initiator, EventData eventData)
Parameters
initiatorICharacterThe character the action is running for.
eventDataEventDataThe event being executed, or null.
Returns
- bool
True when the local peer is a client.
Remarks
For actions whose entire effect is presentational: particles, sound, floating text, camera shake. Those must run on every client that can see the event and on NONE of the server, which has nothing to render and would spend the allocation for no viewer.
The mirror of IsServer(ICharacter), and not a substitute for it. This answers "should I draw something", never "may I change state". An action that mutates anything must still gate on EcaAuthority; gating a state change on this instead would let every client change state and the server change none.
There is no host mode in this project, so a peer is a client or a server and never both. A character with no networked identity — a scene-authored trigger, an edit-mode test — answers true, matching the "allow when undecidable" stance the authority gate takes.
IsReplayTick(EventData)
True when this execution is a prediction REPLAY rather than a first run.
protected static bool IsReplayTick(EventData eventData)
Parameters
eventDataEventDataThe event being executed, or null.
Returns
- bool
True when the event carries a replayed tick.
Remarks
A reconcile replays every tick between the corrected one and now, so anything an action
does that is visible or one-shot — a floating number, a VFX instance, a sound — fires
once per replayed tick unless it is suppressed here. That is the visual spam
PlayFXAction already guards against; this is the same test with one definition
instead of an open-coded copy per action.
Never use this as an authority gate. The server's own ability dispatches carry replicate ticks too, so gating state changes on it suppresses the server and the effect happens nowhere — the exact mistake EcaAuthority exists to prevent. This answers "have I already done this once", not "am I allowed to do this".
IsServer(ICharacter)
True when this action is running on a peer that is actually hosting a server.
protected static bool IsServer(ICharacter initiator)
Parameters
initiatorICharacterThe character the action is running for.
Returns
- bool
True when the local peer's server is initialized.
Remarks
The runtime replacement for #if UNITY_SERVER, which every server-only action body
used to be wrapped in. UNITY_SERVER is a build-target define, so it is undefined
in the editor — and the scene server runs from the editor. Every one of those bodies
therefore compiled to nothing there, which meant a bindstone bound nobody, a merchant
opened no shop, and a teleporter moved no one, in the configuration the project is
developed in. It is the same compile-time gate already found and removed from
NPC.OnStartServer and from scene object registration.
Asks the initiator's own FishNet.Object.NetworkObject rather than a global singleton, so a client-hosted process answers for the peer the character belongs to.
SendToOwner<T>(ICharacter, T)
Sends a broadcast to the one player an action is acting for.
protected static void SendToOwner<T>(ICharacter character, T message) where T : struct, IBroadcast
Parameters
characterICharacterThe character whose owning connection should receive it.
messageTThe broadcast to send.
Type Parameters
TThe broadcast type.
Remarks
Not initiator.NetworkObject.Broadcast(...), which is what these actions
used to call. That sends to the observers of the initiator's NetworkObject —
every client that can see the player, not the player. One person opening a merchant
therefore opened the shop on every screen within observer range, because the client
handlers have no reason to filter a message they were sent directly.
Anything that opens a window, offers a quest, or reports a personal result belongs here. Genuine world state — a switch throwing, a door opening — should go to the observers of the object that changed, which is a different set again.
TryResolveTarget(EventData, out ICharacter)
Strict target resolution: returns the explicit TargetCharacter only. Does not fall back to the initiator — outward-effecting actions (damage, dispel, interrupt, knockback) should use this so a misconfigured selector can't silently make a caster attack itself.
protected static bool TryResolveTarget(EventData eventData, out ICharacter target)
Parameters
eventDataEventDataThe action's event data (may be null).
targetICharacterResolved target character, or null when none is present.
Returns
- bool
True when an explicit target was resolved.
TryResolveTargetOrInitiator(ICharacter, EventData, out ICharacter)
Forgiving target resolution: prefers TargetCharacter, then falls back to the initiator. Use for self-effecting actions (resource costs, self-buffs, cooldown starts) where "no target" naturally means "act on self". Outward-effecting actions should use TryResolveTarget(EventData, out ICharacter) instead so a missing target produces a no-op rather than a self-hit.
protected static bool TryResolveTargetOrInitiator(ICharacter initiator, EventData eventData, out ICharacter target)
Parameters
initiatorICharacterThe action's initiator.
eventDataEventDataThe action's event data (may be null).
targetICharacterResolved character, or null when neither is available.
Returns
- bool
True when a non-null target was resolved.