◂ 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
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.
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
| CONJUNTO | ATRIBUTO | META? | POR QUE EXISTE |
|---|---|---|---|
| HealthSet | Health | vida atual | |
| MaxHealth | separado pra GE e UI poderem ler e modificar | ||
| Damage | meta | dano entrando → vira -Health | |
| Healing | meta | cura entrando → vira +Health | |
| CombatSet | BaseDamage / BaseHeal | capturados da origem pela execution | |
| ResourceSet | Stamina / MaxStamina / StaminaRegen | só se stamina realmente travar habilidade | |
| DefenceSet | Armour / MagicResist | dois, não oito | |
| MovementSet | MoveSpeedMultiplier | multiplicador, padrão 1.0 |
- 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
CharacterMovementComponentreexecuta 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 deGetMaxSpeed()em vez de cachear emMaxWalkSpeed.
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
| CALLBACK | O QUE PÔR | BUG SE ERRAR |
|---|---|---|
| PreAttributeBaseChange | clampar o base | O 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 |
| PreAttributeChange | clampar o current, e nada mais | buff empurra vida acima do máximo; cura excedente gruda |
| PostGameplayEffectExecute | converter meta-atributo, clamp final, morte | dano nunca vira -Health; meta-atributo acumula |
| PostAttributeChange | reconciliação entre atributos | MaxHealth 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".
- 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 porSetNumericAttributeBase, então disparaPreAttributeBaseChange. Chamar o setter de dentro dele recursiona até estourar a pilha.
Replicação
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, mira | Stamina, MaxStamina, StaminaRegen |
| MoveSpeedMultiplier — proxies precisam concordar ou o CMC corrige | BaseDamage, 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.
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);
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.
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
GameplayCueManager.cpp:50 — AbilitySystem.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
- PlayerState replica a 1 Hz — corrigir sempre.
- Clampar em só um dos
Pre*→ base vaza. - MaxHealth diminuindo não clampa Health — precisa de
PostAttributeChange. - Clamp não persiste nos agregadores — Executions e MMCs precisam clampar de novo.
- Faltar
REPNOTIFY_Always→ atributo predito não reconcilia, silenciosamente. - Faltar
HideFromModifiers→ alguém ligaDamagedireto e pula a mitigação. - Meta-atributo não zerado → valor velho, debug ilegível.
- Meta-atributo em GE de duração →
PostGameplayEffectExecutenunca dispara. No-op silencioso. - Replicar meta-atributo → banda jogada fora e
OnRepsem sentido. - Regeneração como GE infinito aditivo → a vida regenerada some quando o efeito é removido. Usar periódico instantâneo.
- Chamar setter dentro de
PreAttributeBaseChange→ recursão infinita. - AttributeSet com outer no pawn em vez do PlayerState → conjunto pendurado no respawn.
ASC->GetOwner()devolve o PlayerState, não o pawn. UsarGetAvatarActorFromActorInfo().GetAvatarActor()fica nulo por um frame depois da morte — proteger todo deref.- Conceder habilidade duas vezes na re-possessão —
GiveAbilitydevolve handle novo; o vazamento é invisível. - 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.
ClearAllAbilities()num ASC compartilhado apaga também equipamento e buffs.ClearAbilityno meio da execução chamaEndAbilitysem replicar — clientes nunca ficam sabendo.RemoveAtSwapdentro deClearAbility— nunca cachear índice; usarABILITYLIST_SCOPE_LOCK().- Mudar
Spec.LevelsemMarkAbilitySpecDirty→ dessincroniza calado. FastReplicationcom tabelas de tag divergentes resolve pra outra tag válida, sem log.NumBitsForContainerSize=6limita a 63 tags — ensure em dev, truncamento silencioso em Shipping.- GameplayCue não roda em servidor dedicado (acima).
ServerSetReplicatedTargetDataé RPC reliable — estouro da fila desconecta o cliente. Dez jogadores mandando array gordo de alvos é jeito realista de chegar lá.- Assinatura de loose tag mudou na 5.7 — o novo parâmetro não replica por padrão;
AddLooseGameplayTagdo servidor passa a não fazer nada no cliente. - 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ó.
- Batching de RPC de habilidade é explicitamente inseguro — "opt in only if you know what you are doing". A GASDocumentation recomenda; não usar.
ReplicateAbilitiesToSimulatedProxiestem padrãofalse, com o comentário "presume-se que seja um bug".
Inicialização de atributos
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".
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
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.
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.
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
- 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_Damagedentro deLyraHealthSet.h, ao lado da classe que a usa. - .ini pra conteúdo de designer, um arquivo por domínio em
Config/Tags/. O nossoConfig/DefaultGameplayTags.iniúnico vira ímã de conflito passando de ~300 tags.
A classe entra no namespace só 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 HABILIDADE | NetExecutionPolicy |
|---|---|
| Ataque básico, esquiva, bloqueio — animação, spammável | LocalPredicted |
| Habilidade de classe com cooldown e dano por Execution | ServerInitiated |
| Consumível, loot, craft, subir de nível — escreve estado persistente | ServerOnly |
| Emote, cosmético | LocalOnly |
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:
ServerInitiatedem vez deLocalPredictedpra habilidade de classe — a cultura padrão do GAS é "predizer tudo".- Deixar
net.UseAdaptiveNetUpdateFrequencydesligado — contraria a GASDocumentation, mas acompanha o padrão da 5.8 e o Lyra. - Pular o sistema de Experience/GameFeature do Lyra.
MoveSpeedMultiplierlido ao vivo emGetMaxSpeed()em vez de cacheado.- Curve table em vez de
AbilitySpec.Levelpra escalar habilidade. - Dois GEs compartilhados de custo/cooldown em vez de um asset por habilidade.