Right way to model per limb health with GAS?

Hello,

We’re working on a multiplayer project with the GAS and we want to implement a localized damage system. Each body part should have its own health and be individually affected by gameplay effects (e.g. poison affects the chest, neurotoxin affects head, fall damage hits the legs), like in Deus Ex or Fallout NV.

Concretely, we want our Health Component (shared by all entities in the game) to configure body parts through Data Assets, then expose a function like: TakeDamage(BodyPartTag, DamageAmount) to be used by various other components (fall damage, weapons, etc.).

We’re struggling to figure out how to cleanly model and route body part data:

  • The ASC does not support multiple Attribute Sets of the same type, so we can’t have one per body part;

  • An Attribute Set cannot contain maps or arrays, so we can’t index body part data this way from the component;

  • Hard-coding body part sets in one or multiple attribute sets would require branching logic in the component and knowledge of all possible body parts, which we want to avoid;

  • We could pass a gameplay tag as parameter to damage gameplay effects, then map tag → attribute data in the attribute set’s post effect callback. This should work, but I’m not sure if it’s a good idea;

  • Store and replicate health in the component instead of GAS and use gameplay effects to mutate the component.

Does anyone know the idiomatic way to achieve what we want? We can get it to work somehow, but I’d like to know the best way. Looking forward to your opinions!

Hi, nice question by the way!

I think you can still work within GAS for this. You can have additional Attribute Sets for body parts per archetype, just not multiple instances of the same set type. The trick is really how you route damage to them.

One approach that works well is forwarding the body part information into an Execution Calculation. Let the execution handle things like damage reduction, armor, and any modifiers first. After that, the execution can look at what attributes the owner actually has and propagate the result to the correct one.

You’ll probably want some kind of naming or convention there (for example sub-health_leg, sub-health_head, etc.) so the execution can find the right attribute. If that attribute exists, apply the damage. If it doesn’t, you can define a fallback rule for what should happen in that case.

About the branching if you are saying having different AttributeSetClasses (ULegAttributeSet , UCommonAttributeSet) is not something desirable maybe can be a single one containing all possible archetype limbs in the game with hardcoding which is imo more uglier. Is there a problem with classes of attributes for each body part? Since its more extendable from my point of view?

From there it’s just normal GAS flow again: the attribute changes, events fire, cues play, gameplay reacts. Lose an arm, trigger a cue, disable an ability, whatever your game needs.

You can also go for a more decoupled setup where body parts are predefined per archetype, matched to bones, and owned by a separate damage component. That component becomes the middleman between hit detection and GAS. It works, but in my experience it’s usually more complex than needed.

Personally I’d start with the first approach. Keeping health in attributes makes it much easier to integrate with the rest of gameplay logic and evolve it over time. For the component depending on t he gameplay logic you want think available attirbutes can be search on construction and health component as middle man tracks that in different archetypes.

You will likely need a separate attribute set per body part if you need to model it that way. You could subclass a base attr set and use that… may give you some benefits of writing for the parent class but passing the child attr set to modify.

Passing data to your GE/GEC/MMC using AssignTagSetByCallerMagnitude will allow you to use specific tags combined with a float value to do whatever processing you need. If you take Grimnir’s advice, you can modify your logic in your GEC depending on what tags get passed with what data. Its a better solution than doing that type of work in the post effect I think.

In my experience any time I’ve straddled using GAS to modify values outside the attr set I’ve ended up pulling the GAS out of it and rewriting to just use standard RPC networking. That being said, I think the use case you outlined fits right in with GAS. Just need a bit of work to figure out how you need to set it up.

1 Like

Thanks for your answer! We decided to do what you suggest and make one attribute set per body part, inheriting an abstract LimbAttributeSet to reduce boilerplate since they all have the same properties.

Using an execution calc for routing the damage to the right body part (using gameplay tags for which bone/collider corresponds to which body part) sounds like a great idea too. We’ll look into it!

1 Like

Using AssignTagSetByCallerMagnitude sounds interesting! I’ll check it out, thanks!

In my experience any time I’ve straddled using GAS to modify values outside the attr set I’ve ended up pulling the GAS out of it and rewriting to just use standard RPC networking

My colleague is planning to do this for our ammo system, since ammo has to be per-weapon, and having one ASC per weapon doesn’t sound advised (and Lyra apparently does it on its own too).

Thats great, think using tags would be better, its dynamic, hardcoding names is not very great right? However it really depends how big the game is, you have humanoids, 4 legged ones, arachnas, monsters then better do tags :slight_smile:

Below is something I did similar just to show, how tags requested from a specific effect and what would happen in result. This is a code block of my game for a test that I did a projectile hit application of vampiric damage over time normally just changed it to get a magnitude and attribute tag and search attribute in another class, then make a change on attribute if found. So don’t be bothered by the name of the class.

// Mono Games Studio

#include "Execution/MGGEC_Vampiric.h"
#include "AbilitySystemComponent.h"
#include "GameplayEffectTypes.h"
#include "MGAttributeHelpers.h"
#include "Kismet/GameplayStatics.h"

UMGGEC_Vampiric::UMGGEC_Vampiric()
{
    LifestealPercent = 1.0f; // default 100% lifesteal
//Seems I was going to make this also tag for different scales of application
}

void UMGGEC_Vampiric::Execute_Implementation(
    const FGameplayEffectCustomExecutionParameters& ExecutionParams,
    FGameplayEffectCustomExecutionOutput& OutExecutionOutput
) const
{
    const FGameplayEffectSpec& Spec = ExecutionParams.GetOwningSpec();
    UAbilitySystemComponent* SourceASC = ExecutionParams.GetSourceAbilitySystemComponent();
    UAbilitySystemComponent* TargetASC = ExecutionParams.GetTargetAbilitySystemComponent();
    if (!TargetASC) return;

    // 1) Magnitude from injected SetByCaller . Basically we are looking for an impact data, did this take damage at all ?
    float Value = Spec.GetSetByCallerMagnitude(
        FGameplayTag::RequestGameplayTag("Gameplay.Data.Magnitude"),
        false,
        0.f
    );
    if (FMath::IsNearlyZero(Value))
    {
        UE_LOG(LogTemp, Warning, TEXT("[Vampiric] No Gameplay.Data.Magnitude in spec"));
        return;
    }

    // 2) Attribute tag from injected DynamicAssetTags, This simply can be the limbs. I look if a tag for specific thing is forwarded to me and search in it. Maybe its not the best way however it works.
    FGameplayTagContainer AssetTags;
    Spec.GetAllAssetTags(AssetTags);

    FGameplayTag AttributeTag;
    for (const FGameplayTag& T : AssetTags)
    {
        if (T.ToString().StartsWith("Gameplay.Data.Attribute"))
        {
            AttributeTag = T;
            break;
        }
    }

    if (!AttributeTag.IsValid())
    {
        UE_LOG(LogTemp, Warning, TEXT("[Vampiric] No Gameplay.Data.Attribute.* tag in spec"));
        return;
    }

    // 3) Ok so we have now the tag execution didn't stop now I go another class which is a dictionary for the attributes. FMGAttributeHelpers this can be the class where searches and holds all attributes of limbs in the actor. It better to be a running one somewhere so it doesn't search something heavy for each hit, which can be very demanding.
    const FGameplayAttribute TargetAttr = FMGAttributeHelpers::ResolveAttributeFromTag(AttributeTag, TargetASC);
    if (!TargetAttr.IsValid()) return;

    OutExecutionOutput.AddOutputModifier(
        FGameplayModifierEvaluatedData(
            TargetAttr,
            EGameplayModOp::Additive,
            Value
        )
    );
}

You can have attributes like

Gameplay.Data.Attribute.Hit.Data.Location.Head
Gameplay.Data.Attribute.Hit.Data.Location.LeftArm
Gameplay.Data.Attribute.Hit.Data.Location.Carapace
Gameplay.Data.Attribute.Hit.Data.Location.Core

Helper can be more nicer for sure to designer add remove attribute tags with ease or a dictionary manager class so they can map whatever tag matches whatever attribute. It can provide some nice playground and experimentation aswell that time. Like 1 tag can effect all ?

  if (AttributeTag == FGameplayTag::RequestGameplayTag("Gameplay.Data.Attribute.Hit.Data.Location.Head"))
    {
        if (FProperty* Prop = BPClass->FindPropertyByName(TEXT("Attr_Health_Head")))
            return FGameplayAttribute(Prop);
    }

Then life is easy,

Hit system : Adds tags magnitude, attribute to an effect spec. This can be a projectile impact, hitscan, overlap of melee whatever. Its a hit. GameplayEffectSpec applied on hit.

Gas (Execution) : Handles the impact, you don’t apply damage directly but let GAS do its job. Resolve tags and make the calculation. Since we are in GAS now it will do its job, validate, run with its own terms and come to execution to do the real job.

The trick over here when you have many execution classes for different aspects handling the order correctly or handling inputs and outputs in order.

PS : For a weapon, projectile and impact system, you don’t really to have ASC per weapon. ASC can be just one on the deployer (character / enemy / object), the tags and data can be carried over to impact without the abilityy system component. Upon impact (if hit and something needs to be done) you can simply go to the instigator ASC who is responsible already for the hit and request an effect to the impacted actor through the impact making actor.

My advice is to have one ASC per weaponCore / Deployer. Deployment can be a single nicely designed ability (preferable a custom one) that runs sub abilities, since deployment styles (actuation,rapid, burst, instant, heating, bolt action, dual action etc.) and methods (hitscan, projectile, continious) can change and child abilities can control those with ease, you don’t really need multiple ASC. Weapons change is a child ability that runs the master weapon ability to do all the black magic.

Happy developing, let us know how it goes.

1 Like