◂ GAMEENGINE.LIZTEM.COM ▸ GAS: DECISÕES DE IMPLEMENTAÇÃO

GAS, decidido.

Pesquisa de 03/08/2026 para a T2. Diferente das outras frentes, esta quase não depende de opinião de comunidade: todas as citações de engine e de Lyra foram lidas direto do código-fonte no tag 5.8.1-release — que agora está clonado nesta máquina. Onde o código contradiz o consenso da internet, o código ganhou.

POSIÇÃO marca recomendação argumentada, não consenso documentado. O resto é código-fonte ou política já embarcada no Lyra.

O ponto de calibragem que muda a conversa

O Lyra 5.8 inteiro roda com DOIS AttributeSets e SEIS atributos

LyraAttributeSet (base), LyraHealthSet, LyraCombatSet. Sem stamina, sem armadura, sem velocidade de movimento, sem nível. A Epic entregou um framework multiplayer completo com seis atributos.

Qualquer tentação de criar FireResist, FrostResist, CritChance, AttackSpeed e afins morre aqui.

E não criar dez AttributeSets, um por classe

Dez conjuntos × dez PlayerStates = cem subobjetos replicados sem nenhum benefício. A diferença entre classes mora no AbilitySet e nas linhas da curve table, nunca na estrutura de atributos.

Os atributos que vamos ter

CONJUNTOATRIBUTOMETA?POR QUE EXISTE
HealthSetHealthvida atual
MaxHealthseparado pra GE e UI poderem ler e modificar
Damagemetadano entrando → vira -Health
Healingmetacura entrando → vira +Health
CombatSetBaseDamage / BaseHealcapturados da origem pela execution
ResourceSetStamina / MaxStamina / StaminaRegensó se stamina realmente travar habilidade
DefenceSetArmour / MagicResistdois, não oito
MovementSetMoveSpeedMultipliermultiplicador, padrão 1.0
Deixado de fora de propósito
  • Resistência por elemento. Elemento vira tag de tipo de dano no spec do GE, lido pela execution contra uma curve table. Elemento novo = uma linha de CSV, não recompilar e mais um float replicado em todo ator do mundo.
  • CritChance, CritMultiplier, AttackSpeed — vão pra execution via curve table, até que algum buff realmente precise mexer neles em combate.
  • MaxWalkSpeed como atributo. O CharacterMovementComponent reexecuta movimentos salvos na correção; se o atributo do cliente divergir do servidor no replay, viram micro-correções permanentes que parecem borracha. POSIÇÃO ler o multiplicador ao vivo dentro de GetMaxSpeed() em vez de cachear em MaxWalkSpeed.
O detalhe que impede um erro silencioso

Meta = (HideFromModifiers) não é cosmético: FGameplayModifierInfo::Attribute carrega meta=(FilterMetaTag="HideFromModifiers") em GameplayEffect.h:570, o que remove o atributo do seletor de Modifier no editor. É a trava mecânica que impede alguém ligar um Modifier direto em Health ou Damage e pular toda a mitigação.

E a UE 5.8 já traz ATTRIBUTE_ACCESSORS_BASIC em AttributeSet.h:465 — a GASDocumentation manda colar a macro em todo header; não precisa.

Onde vai o clamp — e qual bug cada erro gera

CALLBACKO QUE PÔRBUG SE ERRAR
PreAttributeBaseChangeclampar o baseO bug nº 1 de GAS. CurrentValue parece certo e o BaseValue vaza. Um buff de +50 MaxHealth expira, o agregador recalcula do base sem clamp, e a vida salta pra um valor absurdo
PreAttributeChangeclampar o current, e nada maisbuff empurra vida acima do máximo; cura excedente gruda
PostGameplayEffectExecuteconverter meta-atributo, clamp final, mortedano nunca vira -Health; meta-atributo acumula
PostAttributeChangereconciliação entre atributosMaxHealth diminuindo não clampa Health — nenhum dos dois Pre* enxerga isso

Contrato da engine, AttributeSet.h:213: "serve pra impor coisas como Health = Clamp(Health, 0, MaxHealth) e NÃO coisas como disparar algo extra quando dano é aplicado".

Duas consequências não óbvias
  • Clampar nesses callbacks não muda o modificador no ASC — só o valor consultado. Qualquer coisa que recalcule a partir dos agregadores (Executions, MMCs) enxerga valor sem clamp e precisa clampar de novo por conta própria.
  • SetHealth() passa por SetNumericAttributeBase, então dispara PreAttributeBaseChange. Chamar o setter de dentro dele recursiona até estourar a pilha.

Replicação

Decisão: Mixed pros jogadores, Minimal pros monstros, nunca Full

Minimal mapeia pra COND_Never inclusive pro cliente dono (GameplayEffect.cpp:5190) — um pawn de jogador em Minimal nunca fica sabendo dos próprios buffs e cooldowns. Full é pra jogo single-player.

O modo de replicação só controla ActiveGameplayEffects. Não toca nos atributos. A alavanca real de banda são as condições por atributo — e o Lyra já faz essa divisão (LyraCombatSet.cpp:23):

COND_None (todos veem)COND_OwnerOnly
Health, MaxHealth — barra de vida do grupo, número de dano, miraStamina, MaxStamina, StaminaRegen
MoveSpeedMultiplier — proxies precisam concordar ou o CMC corrigeBaseDamage, BaseHeal, Armour, MagicResist, CharacterLevel
Meta-atributos: não replicam

REPNOTIFY_Always é obrigatório em qualquer coisa predita — sem ele o OnRep é pulado quando o valor replicado é igual ao predito localmente, e o base nunca reconcilia.

A armadilha do PlayerState: ele replica a 1 Hz

APlayerState::APlayerState chama SetNetUpdateFrequency(1) (PlayerState.cpp:28). Um hertz. Tudo de GAS parece quebrado até você corrigir:

AbilitySystemComponent->SetIsReplicated(true);
AbilitySystemComponent->SetReplicationMode(EGameplayEffectReplicationMode::Mixed);
SetNetUpdateFrequency(66.0f);      // Lyra usa 100; 66 sobra com 10 jogadores
SetMinNetUpdateFrequency(10.0f);
Iris desligado, ReplicationGraph desligado, proxy da Fortnite não construir

AbilitySystem.Fix.ReplicateTagCountContainerWithIris tem padrão false, com o comentário no próprio código: "a replicação do Iris não funciona igual, então estamos voltando pro Minimal e loose tags replicadas até consertarem".

E ninguém publicou dado de banda de GAS com 5–10 jogadores. O artigo de proxy de replicação mais citado não contém nenhuma medição e é explicitamente escrito para cenários de 100+ jogadores.

A banda real vai ser dos monstros, não dos jogadores: APawn tem padrão de 100 Hz (Pawn.cpp:88). Com ~100 monstros, baixar lixo pra 20–30 Hz e ajustar NetCullDistanceSquared.

Correção a um mito muito repetido

O "crash do modo Mixed" que todo mundo cita é do caminho legado, desligado por padrão desde a 5.3. A exigência de posse continua valendo, mas a falha agora é silenciosa: se o dono do ASC não encadear até um PlayerController naquela conexão, o cliente dono não recebe GameplayEffect nenhum — UI de cooldown em branco, buff que nunca expira, atributo à deriva. Muito mais difícil de diagnosticar que o antigo crash.

A armadilha nº 1 do nosso servidor dedicado

GameplayCue NÃO executa em servidor dedicado

GameplayCueManager.cpp:50AbilitySystem.GameplayCue.RunOnDedicatedServer tem padrão 0, e ShouldSuppressGameplayCues sai cedo em IsRunningDedicatedServer().

Qualquer lógica de jogo colocada dentro de um Cue funciona perfeitamente no editor e não faz absolutamente nada no servidor de São Paulo.

Regra para a T2: Cue é cosmético. Ponto. É o formato de bug que sobrevive a todo teste local.

As 28 armadilhas

  1. PlayerState replica a 1 Hz — corrigir sempre.
  2. Clampar em só um dos Pre* → base vaza.
  3. MaxHealth diminuindo não clampa Health — precisa de PostAttributeChange.
  4. Clamp não persiste nos agregadores — Executions e MMCs precisam clampar de novo.
  5. Faltar REPNOTIFY_Always → atributo predito não reconcilia, silenciosamente.
  6. Faltar HideFromModifiers → alguém liga Damage direto e pula a mitigação.
  7. Meta-atributo não zerado → valor velho, debug ilegível.
  8. Meta-atributo em GE de duração → PostGameplayEffectExecute nunca dispara. No-op silencioso.
  9. Replicar meta-atributo → banda jogada fora e OnRep sem sentido.
  10. Regeneração como GE infinito aditivo → a vida regenerada some quando o efeito é removido. Usar periódico instantâneo.
  11. Chamar setter dentro de PreAttributeBaseChange → recursão infinita.
  12. AttributeSet com outer no pawn em vez do PlayerState → conjunto pendurado no respawn.
  13. ASC->GetOwner() devolve o PlayerState, não o pawn. Usar GetAvatarActorFromActorInfo().
  14. GetAvatarActor() fica nulo por um frame depois da morte — proteger todo deref.
  15. Conceder habilidade duas vezes na re-possessão — GiveAbility devolve handle novo; o vazamento é invisível.
  16. GE passivo empilhando entre respawns — descartar os handles transforma +10% de velocidade em +100% na décima onda. Este morde sessão longa de co-op.
  17. ClearAllAbilities() num ASC compartilhado apaga também equipamento e buffs.
  18. ClearAbility no meio da execução chama EndAbility sem replicar — clientes nunca ficam sabendo.
  19. RemoveAtSwap dentro de ClearAbility — nunca cachear índice; usar ABILITYLIST_SCOPE_LOCK().
  20. Mudar Spec.Level sem MarkAbilitySpecDirty → dessincroniza calado.
  21. FastReplication com tabelas de tag divergentes resolve pra outra tag válida, sem log.
  22. NumBitsForContainerSize=6 limita a 63 tags — ensure em dev, truncamento silencioso em Shipping.
  23. GameplayCue não roda em servidor dedicado (acima).
  24. ServerSetReplicatedTargetData é RPC reliable — estouro da fila desconecta o cliente. Dez jogadores mandando array gordo de alvos é jeito realista de chegar lá.
  25. Assinatura de loose tag mudou na 5.7 — o novo parâmetro não replica por padrão; AddLooseGameplayTag do servidor passa a não fazer nada no cliente.
  26. Predição cobre bem menos do que se imagina: Executions de GE, remoção de GE e tick periódico não são preditos, e a janela é de um frame só.
  27. Batching de RPC de habilidade é explicitamente inseguro — "opt in only if you know what you are doing". A GASDocumentation recomenda; não usar.
  28. ReplicateAbilitiesToSimulatedProxies tem padrão false, com o comentário "presume-se que seja um bug".

Inicialização de atributos

O que o Lyra faz — e por que NÃO copiar

Em LyraHealthComponent.cpp:82, presente e inalterado da 5.0 até a 5.8.1:

// TEMP: Reset attributes to default values.  Eventually this will be driven by a spread sheet.
AbilitySystemComponent->SetNumericAttributeBase(
    ULyraHealthSet::GetHealthAttribute(), HealthSet->GetMaxHealth());

Esse // TEMP: sobreviveu a oito versões. É um provisório que a Epic nunca removeu.

E o caminho por DataTable a própria engine desaconselha, em AbilitySystemComponent.h:179: "Não bem suportado; um gameplay effect com referência a curve table provavelmente é melhor solução".

Nosso caminho

Um asset GE_ClassInit instantâneo, um Modifier por atributo, cada magnitude sendo um FScalableFloat apontando pra CT_ClassStats, com linhas Bulwark.MaxHealth, Chirurgeon.MaxHealth… e colunas por nível. Entregue como GrantedGameplayEffects[0] no DataAsset da classe.

Dez classes × N níveis = um CSV versionável em texto, sem recompilar. Mantendo defaults não-zero no construtor como rede de segurança, pra que linha faltando nunca vire MaxHealth = 0.

Um DataAsset por classe: o padrão AbilitySet

Portar ULyraAbilitySet quase literal — ~200 linhas, quase sem dependência

A ordem de concessão é AttributeSets → habilidades → efeitos, e ela importa: o GE de inicialização precisa cair em atributos que já existem.

⚠️ GetDynamicSpecSourceTags() substituiu AbilitySpec.DynamicAbilityTags na 5.5. Qualquer tutorial que ainda escreva DynamicAbilityTags é anterior à 5.5. Essa linha sozinha é todo o mecanismo de input tag.

Desvio obrigatório em relação ao Lyra

ALyraPlayerState::SetPawnData concede com OutGrantedHandles = nullptr — de propósito, porque no Lyra é permanente. Nós precisamos guardar os handles, porque temos troca de classe e persistência. Guardar FMgAbilitySet_GrantedHandles no PlayerState e chamar TakeFromAbilitySystem antes de conceder outra classe.

Pular isso é exatamente a armadilha nº 16 — o buff passivo que vira +100% na décima onda.

O que NÃO portar POSIÇÃO

A camada de Experience/GameFeature do Lyra existe pra Epic embarcar modos carregáveis independentemente, feitos por times separados — o Lyra liga 56 plugins. Nós temos um modo. PawnData escolhido pelo GameMode é a altitude certa, e o AbilitySet é justamente a costura que permite adotar Experiences depois, se um dia precisar.

GameplayTags: como não virar bagunça com dez classes

Os dois caminhos, divididos por dono
  • Tag nativa em C++ pra tudo que o código lê ou escreve. O Lyra migrou na 5.3→5.4 do singleton FLyraGameplayTags::Get() pra namespace + UE_DECLARE_GAMEPLAY_TAG_EXTERN, porque o singleton obrigava escrever cada tag duas vezes numa função central — ponto permanente de conflito de merge.
  • Declarar a tag no header que a possui, não num arquivo-deus. O Lyra declara TAG_Gameplay_Damage dentro de LyraHealthSet.h, ao lado da classe que a usa.
  • .ini pra conteúdo de designer, um arquivo por domínio em Config/Tags/. O nosso Config/DefaultGameplayTags.ini único vira ímã de conflito passando de ~300 tags.
A regra de nomenclatura que evita o alastramento

A classe entra no namespace para coisas que existem uma vez por classe (Cooldown.Chirurgeon.Cura). A classe nunca entra acima de um verbo: escrever Ability.Type.Spell, não Ability.Chirurgeon.Type.Spell — senão toda regra de bloqueio precisa ser escrita dez vezes.

Manter 3–4 segmentos: HasTag varre GameplayTags e depois ParentTags, e uma tag de 4 níveis contribui com 3 pais. Dez classes deve dar ~250–350 tags — confortável.

Predição: a recomendação contra a corrente POSIÇÃO

TIPO DE HABILIDADENetExecutionPolicy
Ataque básico, esquiva, bloqueio — animação, spammávelLocalPredicted
Habilidade de classe com cooldown e dano por ExecutionServerInitiated
Consumível, loot, craft, subir de nível — escreve estado persistenteServerOnly
Emote, cosméticoLocalOnly

O raciocínio (GameplayPrediction.h:36, inalterado até a 5.8): o GAS prediz ativação, modificadores de atributo, tags, cues e montagens. Não prediz Executions de GE, remoção de GE, nem tick periódico. E a janela é de um frame: "quando ActivateAbility termina, sua janela de predição não vale mais".

O nosso modelo de dano vai usar Execution (armadura, resistência, crítico, tipo de dano × dez classes). Isso nunca ia ser predito. Predizer uma habilidade de classe compra o início da animação e o desconto do custo — e o início da animação dá pra ter de graça, tocando localmente e deixando o servidor confirmar. Em troca a gente herdaria tremida de rollback em custo e cooldown, que em co-op parece bug, não latência.

O próprio Lyra embarca PredictTargetGameplayEffects=False e envolve ULyraDamageExecution::Execute_Implementation inteiro em #if WITH_SERVER_CODE. Em PvE não existe incentivo a trapaça que justifique a complexidade — e temos persistência autoritativa no servidor, onde errar a predição é problema de correção, não de sensação.

As seis posições argumentadas

Onde a recomendação é julgamento e não consenso documentado — para revisitar se der errado:

  1. ServerInitiated em vez de LocalPredicted pra habilidade de classe — a cultura padrão do GAS é "predizer tudo".
  2. Deixar net.UseAdaptiveNetUpdateFrequency desligado — contraria a GASDocumentation, mas acompanha o padrão da 5.8 e o Lyra.
  3. Pular o sistema de Experience/GameFeature do Lyra.
  4. MoveSpeedMultiplier lido ao vivo em GetMaxSpeed() em vez de cacheado.
  5. Curve table em vez de AbilitySpec.Level pra escalar habilidade.
  6. Dois GEs compartilhados de custo/cooldown em vez de um asset por habilidade.