New Ability Archetype Recipe¶
How to add a new authorable ability archetype (buff, aura, channel, beam, …) to the AbilityAuthoringWorkflow pipeline. The Leap archetype (Crash Landing) is the worked example — it was built by exactly this recipe.
Policy gate first (L7)¶
A new C++ base class exists only for a new VERB — a movement/spawn/targeting behavior the engine must physically perform (leap trajectory, beam trace, channel loop). Before writing any C++:
- New parameters on an existing verb → CDO fields + a catalog row. No new class.
- New combination of existing verbs → a Blueprint child or composed helpers. No new class.
- Cost/cooldown/tags/cues/scaling are never a reason for a new base — that's the shared-GE + SetByCaller layer every player-activated base already carries.
- Keep the hierarchy shallow: prefer extracted helpers (
FAOEStaticspattern) over deep subclassing.
Also the GDD authoring rule (plan L6): the archetype must serve a committed Phase plan — nothing is authored around unbuilt features. Commit the plan (One-Pager + phase section) before code.
The recipe¶
1. Base class (only if the verb is new)¶
- Subclass the branch that already carries the closest contract. Player-activated skills subclass
USkillAbility/UAOESkillAbilityso the shared SetByCaller cost GEs (Ge_ResonanceCost_Dynamic/Ge_StaminaCost_Dynamic),GE_Cooldown_Shared, and theFScalableFloatmagnitude fields (L8) are inherited, not re-implemented. Procs subclassUOnHitAbility/UOnDamagedAbilityfor the trigger/chance/condition contract. - Add only the fields the first real ability uses (speculative fields get deleted, not wired).
Tuned magnitudes are
FScalableFloat; feel constants (arc height) stay plain floats. - Pin
InstancingPolicy/NetExecutionPolicyin C++ if you deviate from the parent (the catalog contract spec bans NonInstanced fleet-wide). - Grant/spawn/damage paths stay authority-guarded; per-activation instance state must be reset in
ActivateAbility(per-actor instances are reused).
2. Sweep every gate for the new shape¶
New data shapes must be taught to every validator/consumer that special-cases the parent
(new-data-shape-sweeps-every-gate). Grep for the parent class name across:
Source/ProjectEternalEditor/Private/Validation/AbilityValidationCore.cpp—CheckMontageWiringis now catalog-driven: set the row'sMontageRoute(below) instead of adding an IsA branch. Leap, for instance, isSkillMontages(fires its AOE at landing, so it validates like a plain skill — a warning, never the AOE-skill spawn-path error). TheUEnemyAbilityfork is the one exception still matched by an explicit class check (its subclasses bucket to the Enemy-family rows; the enemy-AOE fork data-drives through its own row since the EnemyAOE row landed). If a genuinely new montage behaviour appears, add anEMontageRoutevalue + a switch case here, then a route pin inAbilityValidation.spec.cpp.- Replace-style overrides on sibling classes (
overrides-that-replace-drop-new-base-gates). - The archetype contract spec (
ProjectEternal.Unit.Abilities.ArchetypeCatalog) — it picks the new row up automatically, but its checks only cover slots you DECLARE (step 4).
3. Headless spec¶
DEFINE_SPEC through the REAL exec path (see LeapAbility.spec.cpp / AOESkillAbility.spec.cpp):
ticking CreateWorld rig, real shared GE assets loaded by path, costs deducted / cooldown expires /
damage lands assertions, refusal paths leave state untouched, L8 curve scaling proven at level 2.
Keep World->Tick steps below 0.4 s (MaxUndilatedFrameTime clamping starves GE timelines).
4. Catalog row (Config/DefaultGame.ini)¶
One +Archetypes=(...) row under [/Script/ProjectEternal.EternalAbilityArchetypeCatalog]:
ArchetypeId, DisplayName, Category, CategoryColor (the Workshop tint — data, not a
GetCategoryColor code edit), BaseClass, DefaultFolder, TemplateBP, bPlayerActivated, MontageRoute
(step 2 — how the montage validator dispatches; omit for None), and the authoring contract —
RequiredTagSlots / RequiredClassSlots / NumericSlots. Declare every slot the archetype's
function depends on — an undeclared slot silently re-opens the "AOE-cost gap" class of bug the
contract spec exists to catch. FindRowForClass is most-derived-wins, so a subclass row shadows its
parent's row automatically.
5. Template asset BP¶
Author TPL_<ArchetypeId> in /Game/Gameplay/GameplayAbilities/Templates/ (folder is in
DirectoriesToNeverCook). Duplicate the best existing exemplar of the archetype if one exists;
author fresh only when the base has no BP children. Wire the full shared contract (cost GEs,
GE_Cooldown_Shared, GE_Damage, sane default magnitudes) so the template is validator-green by
construction — the wizard seeds every new ability from it.
6. Exemplar via the wizard/stamp¶
Author the first real ability from its One-Pager through Eternal.Ability.Stamp (or the Workshop
wizard — same code path). Register its tags in Config/Tags/GameplayTags.ini (cooldown tag, asset
tag; cue tags belong to the VFX hand-finish pass with their notifies). Validate: 0 errors. Montage,
notifies, cues, and feel remain hand-work — the pipeline does plumbing only.
7. Prove it¶
Full test suite green (the contract spec now loads your template), validation sweep 0 errors, and a PIE grant through a kit/preset if the ability is player-facing.
Worked examples (Leap, EchoStrike)¶
| Step | Leap artifact | EchoStrike artifact |
|---|---|---|
| Verb | ULeapAbility — launch arc + LandedDelegate impact (Abilities/LeapAbility.h) |
UEchoStrikeAbility + AEchoStrikeActor — server-spawned replicated phantom repeats the owner's strike (Abilities/EchoStrikeAbility.h, Combat/EchoStrike/) |
| Gate sweep | CheckMontageWiring leap route (SkillMontages) |
MontageRoute=None (proc); no-recursion guard native in ShouldProc |
| Spec | ProjectEternal.Unit.Combat.LeapAbility (7 cases) |
ProjectEternal.Unit.Combat.EchoStrikeAbility (spawn/damage, proc-stream mark, no-chain) |
| Row | ArchetypeId="Leap", NumericSlots include LeapDistance |
ArchetypeId="EchoStrike", RequiredClassSlots PhantomActorClass+PhantomDamageEffectClass |
| Template | TPL_Leap (authored fresh — new base, no BP children) |
TPL_EchoStrike (authored fresh — new base) |
| Exemplar | GA_CrashLanding from Design/Abilities/CrashLanding.ability.md |
GA_EchoStrike from Design/Abilities/EchoStrike.ability.md |