← Voltar para artigos
Game Development Publicado em 21 de jul. de 2026 9 min de leitura

Configurando um sistema de combate com o GAS

Ao pensar em uma arquitetura de combate, o Gameplay Ability System (GAS) é o framework que vem à minha mente. Neste artigo, mostro alguns desafios que encontrei durante a configuração do GAS no projeto Oathbreakers.

  • Unreal Engine 5
  • C++
  • Gameplay Ability System
  • Arquitetura de Gameplay
  • Multiplayer
Afonso Pordeus Estudos em Unreal Engine, C++ e desenvolvimento de jogos.
Guerreiro diante de uma fortaleza em chamas, ao lado do título GAS Gameplay Ability System.
Game Development
Neste estudo11 seções

Ao começar a desenvolver o sistema de combate de Oathbreakers, uma das primeiras decisões importantes foi escolher como as habilidades, os atributos e os efeitos aplicados aos personagens seriam organizados.

Ataques, esquivas, bloqueios, parries, custos de stamina, dano, postura e estados temporários fazem parte do mesmo sistema de combate, mas possuem responsabilidades diferentes. Implementar todas essas mecânicas diretamente nas classes dos personagens rapidamente criaria dependências difíceis de manter.

Por isso, decidi utilizar o Gameplay Ability System, conhecido como GAS.

O GAS é um framework disponibilizado pela Unreal Engine para a construção de habilidades, atributos e efeitos de gameplay. Ele é utilizado principalmente em jogos que possuem sistemas de combate complexos, multiplayer, efeitos temporários, diferentes estados e grande quantidade de interações entre personagens.

Entretanto, começar a utilizar o GAS não significa apenas criar uma habilidade e ativá-la.

Antes de implementar as mecânicas, é necessário tomar decisões sobre onde o sistema ficará armazenado, como será inicializado e quais responsabilidades pertencerão aos players, inimigos e bosses.

Neste artigo, apresento algumas dessas decisões e os desafios que encontrei durante a configuração do sistema de combate de Oathbreakers.

O que é o GAS?

O Gameplay Ability System não representa apenas um sistema de habilidades.

Ele é um conjunto de estruturas que trabalham em conjunto para organizar diferentes partes do gameplay. Entre seus principais elementos estão:

  • AbilitySystemComponent;
  • GameplayAbility;
  • GameplayEffect;
  • AttributeSet;
  • GameplayTag;
  • GameplayCue.

Cada uma dessas estruturas possui uma responsabilidade específica.

O AbilitySystemComponent, ou ASC, funciona como o centro do sistema. Ele armazena habilidades, efeitos ativos, tags e informações necessárias para que um personagem participe do GAS.

Uma GameplayAbility representa uma ação que pode ser executada, como um ataque, uma esquiva, uma magia ou um bloqueio.

Um GameplayEffect modifica valores ou estados. Ele pode causar dano, consumir stamina, aumentar velocidade, aplicar uma tag temporária ou alterar um atributo permanentemente.

Os AttributeSets armazenam os atributos utilizados pelo sistema, como vida, stamina, postura, defesa e poder de ataque.

As GameplayTags representam estados e identificadores. Elas podem indicar, por exemplo, que um personagem está atacando, bloqueando, atordoado ou impedido de utilizar alguma habilidade.

Por fim, os GameplayCues são utilizados principalmente para representar consequências visuais e sonoras dos efeitos de gameplay, como partículas, sons, impactos e alterações temporárias na aparência.

Um fluxo comum pode ser representado da seguinte maneira:

Input

AbilitySystemComponent

GameplayAbility

GameplayEffect

Attribute

GameplayCue

Essa sequência não precisa acontecer completamente em todas as habilidades. Uma habilidade pode não modificar atributos, enquanto outra pode não utilizar um Gameplay Cue.

A principal ideia é que cada camada seja responsável por uma parte do comportamento.

O input solicita uma ação. A habilidade controla a execução. O efeito representa a alteração de gameplay. O atributo armazena o valor, e o Gameplay Cue comunica visualmente o resultado.

Essa separação foi um dos principais motivos para escolher o GAS para o Oathbreakers.

Definição de mecânicas do Oathbreakers

Oathbreakers é um action RPG isométrico com foco em combate contra inimigos e bosses, podendo ser jogado cooperativamente.

Desde o início, o sistema de combate foi pensado para possuir mecânicas como:

  • ataques leves e pesados;
  • esquiva;
  • sprint;
  • bloqueio;
  • parry;
  • consumo e regeneração de stamina;
  • sistema de postura;
  • dano e resistências;
  • habilidades específicas de bosses;
  • efeitos temporários;
  • multiplayer autoritativo.

Mesmo que algumas dessas mecânicas ainda estejam em diferentes estágios de desenvolvimento, elas precisam compartilhar uma mesma base.

Um ataque, por exemplo, pode possuir uma animação, um custo de stamina, uma janela de impacto, aplicação de dano e um efeito visual. Uma esquiva pode consumir stamina, alterar temporariamente o estado do personagem e bloquear determinadas habilidades durante sua execução.

Com o GAS, esses comportamentos podem ser divididos em etapas.

Uma habilidade de ataque pode:

  1. verificar se o personagem possui stamina;
  2. bloquear habilidades incompatíveis;
  3. reproduzir uma animação;
  4. abrir uma janela de ataque;
  5. aplicar um Gameplay Effect de dano;
  6. finalizar a habilidade;
  7. remover as tags utilizadas durante a execução.

Essa estrutura evita que a classe do personagem precise controlar diretamente todo o funcionamento da mecânica.

Entretanto, antes de implementar essas habilidades, encontrei um problema mais fundamental: players, inimigos e bosses não inicializavam o Ability System da mesma maneira.

Por que o PlayerState muda a arquitetura

No Oathbreakers, todos os personagens de combate derivam de uma classe base.

Essa hierarquia permite compartilhar comportamentos comuns, como:

  • acesso aos atributos;
  • aplicação de dano;
  • estados de morte;
  • interação com o sistema de habilidades;
  • informações necessárias para a interface;
  • componentes relacionados ao combate.

Porém, o AbilitySystemComponent não está armazenado no mesmo lugar para todos os personagens.

Nos inimigos e bosses, o ASC pertence ao próprio Character.

No player, ele pertence ao PlayerState.

Essa diferença existe principalmente por causa do multiplayer e do ciclo de vida do personagem.

O Character representa o corpo controlado pelo jogador no mundo. Esse corpo pode ser destruído e recriado durante um respawn, uma troca de personagem ou uma transição entre estados da partida.

O PlayerState representa o estado do jogador dentro daquela sessão. Ele pode continuar existindo mesmo quando o personagem controlado é substituído.

Ao armazenar o ASC no PlayerState, atributos, habilidades e efeitos podem continuar associados ao jogador quando seu Avatar muda.

Essa decisão é útil para o multiplayer, mas também altera completamente a inicialização do sistema.

Para um inimigo, o próprio personagem possui o componente e executa as habilidades.

Para o player, o PlayerState possui o componente, mas quem executa as habilidades fisicamente no mundo é o Character.

Foi nesse ponto que precisei compreender melhor a diferença entre Owner Actor e Avatar Actor.

Owner Actor e Avatar Actor

Ao inicializar o Ability System, o GAS precisa conhecer dois objetos importantes:

  • Owner Actor;
  • Avatar Actor.

O Owner Actor é o objeto que possui ou representa a persistência do Ability System.

O Avatar Actor é o objeto utilizado naquele momento para executar as habilidades no mundo.

No caso de um inimigo, os dois papéis normalmente pertencem ao próprio Character:

Owner Actor: EnemyCharacter
Avatar Actor: EnemyCharacter

A inicialização pode ser feita desta forma:

AbilitySystemComponent->InitAbilityActorInfo(EnemyCharacter, EnemyCharacter);

O primeiro argumento representa o Owner, enquanto o segundo representa o Avatar.

Para o player, a relação é diferente:

Owner Actor: PlayerState
Avatar Actor: PlayerCharacter

A inicialização precisa utilizar os dois objetos:

AbilitySystemComponent->InitAbilityActorInfo(PlayerState, PlayerCharacter);

Nesse caso, o PlayerState mantém o sistema, enquanto o Character funciona como o corpo que executa as habilidades.

Essa diferença parece pequena quando observada apenas na chamada da função, mas possui consequências para toda a arquitetura.

Se a classe base inicializasse automaticamente todos os personagens utilizando Character, Character, ela configuraria corretamente os NPCs, mas configuraria o player de forma incorreta.

A responsabilidade da classe base

Minha primeira ideia foi inicializar o Ability System diretamente na classe base de combate.

Isso parecia coerente porque todos os personagens utilizavam GAS. Porém, a classe base não possuía informação suficiente para tomar essa decisão.

Ela sabia que o personagem participava do sistema de combate, mas não sabia necessariamente quem deveria ser o Owner do ASC.

A solução foi separar duas responsabilidades diferentes:

  1. configurar a relação entre o ASC, o Owner e o Avatar;
  2. executar as etapas comuns após essa configuração.

A classe base possui funções compartilhadas, como:

virtual void InitializeAbilityActorInfo();

void InitializeDefaultAttributes();
void GrantStartupAbilities();
void BindAbilitySystemDelegates();

A função InitializeAbilityActorInfo pode ser sobrescrita pelas classes derivadas, pois cada tipo de personagem conhece sua própria estrutura.

As outras etapas podem permanecer na classe base, desde que sejam chamadas somente depois que o ASC estiver corretamente inicializado.

Essa separação permite que a classe base continue concentrando comportamentos comuns sem assumir que todos os personagens possuem o mesmo ciclo de vida.

Também evita uma situação em que o player seria inicializado primeiro como NPC e depois teria seu Ability Actor Info substituído.

Inicialização dos NPCs

Nos inimigos, o fluxo de inicialização é mais direto.

Como o ASC pertence ao próprio Character, o personagem pode utilizar a si mesmo como Owner e Avatar:

void AOathEnemyCharacter::InitializeAbilityActorInfo()
{
    AbilitySystemComponent->InitAbilityActorInfo(this, this);

    InitializeDefaultAttributes();
    GrantStartupAbilities();
    BindAbilitySystemDelegates();
}

O mesmo modelo pode ser utilizado por bosses quando eles também armazenam o Ability System no próprio personagem.

Nesse caso, inimigos e bosses compartilham a mesma configuração estrutural, mesmo que possuam habilidades, atributos e comportamentos diferentes.

Um boss pode possuir:

  • habilidades exclusivas;
  • Attribute Sets adicionais;
  • fases de combate;
  • mecânicas de postura específicas;
  • estados controlados pela arena;
  • Gameplay Effects próprios.

A posição do ASC não determina quais mecânicas o personagem possui. Ela determina apenas quem mantém o sistema e qual objeto está atuando como Avatar.

A evolução dessa arquitetura que percebi ao estudar o projeto Lyra seria criar uma classe intermediária para personagens cujo ASC pertence ao próprio Character:

CombatCharacterBase
├── PlayerCharacter
└── AbilityCharacterBase
    ├── EnemyCharacter
    └── BossCharacter

Ainda estou avaliando se essa camada realmente reduziria complexidade ou apenas aumentaria a quantidade de classes.

A criação de uma nova abstração só é útil quando ela representa uma diferença real e repetida dentro do projeto.

Inicialização dos players

A inicialização do player exige mais cuidado porque o PlayerState pode estar disponível em momentos diferentes no servidor e no client.

No servidor, o personagem normalmente pode inicializar o Ability System quando é possuído pelo PlayerController.

No client, o PlayerState precisa ser replicado antes que o personagem consiga acessar corretamente o ASC.

Por isso, eventos diferentes podem chamar uma mesma função de inicialização.

A ideia seria basicamente assim:

void AOathPlayerCharacter::InitializeAbilityActorInfo()
{
    AOathPlayerState* OathPlayerState = GetPlayerState<AOathPlayerState>();

    if (!OathPlayerState) return; // Validamos se o PlayerState é válido

    AbilitySystemComponent = OathPlayerState->GetAbilitySystemComponent(); // PlayerCharacter recebendo o ASC do PlayerState

    CombatAttributeSet = OathPlayerState->GetAttributeSet(); // O ASC tem acesso aos atributos, acho que vou alterar isso mais pra frente retirando esse Getter

    AbilitySystemComponent->InitAbilityActorInfo(OathPlayerState, this); // this = AOathPlayerCharacter

    InitializeDefaultAttributes(); // Inicialização dos atributos com GameplayEffects
    GrantStartupAbilities(); // Recebimento das habilidades padrões
    BindAbilitySystemDelegates(); // Vincula os delegates no final da inicialização
}

No servidor, essa função pode ser chamada durante o PossessedBy:

void AOathPlayerCharacter::PossessedBy(AController* NewController)
{
    Super::PossessedBy(NewController);

    InitializeAbilityActorInfo();
}

No client, ela pode ser chamada quando o PlayerState for replicado:

void AOathPlayerCharacter::OnRep_PlayerState() // <- Essa função é chamada quando o PlayerState é replicado
{
    Super::OnRep_PlayerState();

    InitializeAbilityActorInfo();
}

Os dois eventos representam momentos diferentes, mas ambos tentam preparar a mesma relação entre PlayerState, ASC e Character.

Essa centralização evita duplicar toda a lógica de inicialização em diferentes funções do ciclo de vida.

Evitando inicializações duplicadas

Ao permitir que mais de um evento chame a inicialização, surge outro problema: algumas operações não podem ser executadas repetidamente.

Atualizar o Ability Actor Info pode ser necessário quando o Avatar muda. Porém, conceder as mesmas habilidades duas vezes ou registrar o mesmo delegate repetidamente pode causar comportamentos incorretos.

Por isso, comecei a tratar a inicialização não como uma única operação, mas como várias etapas com regras diferentes.

Por exemplo:

  • o Ability Actor Info pode ser atualizado quando necessário;
  • habilidades iniciais devem ser concedidas somente pelo servidor;
  • atributos iniciais não devem ser reaplicados sem necessidade;
  • delegates precisam evitar registros duplicados;
  • widgets precisam reagir quando o ASC estiver disponível;
  • referências ao Avatar precisam ser atualizadas após respawns.

A concessão de habilidades pode utilizar uma verificação de autoridade:

void AOathCombatCharacterBase::GrantStartupAbilities()
{
    if (!HasAuthority() || !AbilitySystemComponent)
    {
        return;
    }

    if (bStartupAbilitiesGranted)
    {
        return;
    }

    bStartupAbilitiesGranted = true;

    // Concessão das habilidades iniciais.
}

Essa flag é apenas uma possível solução. Dependendo da estrutura, também é possível verificar as habilidades existentes ou utilizar uma classe específica para representar conjuntos de habilidades.

O mais importante é definir quais operações são idempotentes, ou seja, quais podem ser chamadas novamente sem alterar o resultado incorretamente.

Essa preocupação se torna ainda mais importante em multiplayer, onde servidor, client, replicação e respawn participam do ciclo de vida do personagem.

Attribute Sets e habilidades iniciais

Os personagens de Oathbreakers compartilham vários atributos de combate.

Entre eles estão:

  • vida;
  • vida máxima;
  • stamina;
  • stamina máxima;
  • regeneração de stamina;
  • postura;
  • postura máxima;
  • regeneração de postura;
  • poder de ataque;
  • defesa;
  • velocidade de movimento.

Esses atributos podem existir em um conjunto comum utilizado por players, inimigos e bosses.

Porém, compartilhar uma base não significa que todos os personagens precisam possuir exatamente os mesmos dados.

O player pode possuir recursos específicos relacionados à interface, progressão ou equipamentos. Um boss pode possuir atributos utilizados por suas fases ou mecânicas exclusivas.

Por isso, organizei o sistema pensando em diferentes AttributeSets, como:

CombatSet
HealthSet
BossSet

O CombatSet armazena dados utilizados pela maioria dos personagens.

Os outros conjuntos existem apenas quando aquela categoria realmente precisa deles.

A mesma lógica pode ser aplicada às habilidades iniciais.

Todos os personagens precisam de uma forma de receber suas habilidades, mas a lista não deve estar fixa na classe base.

Um player pode receber:

GA_Player_AttackLight
GA_Player_Dodge
GA_Sprint
GA_Parry

Um boss pode receber:

GA_Boss_GroundSlam
GA_Boss_AttackSequence
GA_Boss_SpecialMechanic

A classe base pode conhecer o processo de concessão, enquanto cada personagem ou configuração define quais habilidades serão concedidas.

Esse modelo permite compartilhar o funcionamento sem misturar o conteúdo de cada personagem.

O que aprendi com essa arquitetura

A principal dificuldade ao começar com o GAS foi entender que o sistema não se resume às Gameplay Abilities.

Antes de implementar ataques e efeitos, é necessário compreender o ciclo de vida do ASC e a relação entre seus diferentes participantes.

Player, inimigo e boss podem utilizar a mesma arquitetura de combate, mas isso não significa que precisam ser inicializados da mesma maneira.

A classe base deve compartilhar apenas o que realmente é comum.

Quando uma diferença representa uma decisão importante do sistema, escondê-la dentro da herança pode tornar o código mais confuso em vez de simplificá-lo.

Algumas regras que pretendo manter no projeto são:

  1. a classe que conhece o Owner e o Avatar deve configurar o Ability Actor Info;
  2. o ASC do player deve permanecer no PlayerState;
  3. NPCs podem utilizar o próprio Character como Owner e Avatar;
  4. habilidades iniciais devem ser concedidas apenas pela autoridade;
  5. cada etapa da inicialização precisa definir se pode ser repetida;
  6. Attribute Sets comuns não impedem a existência de conjuntos especializados;
  7. Gameplay Abilities devem controlar ações, não armazenar todas as regras do personagem;
  8. Gameplay Effects devem representar alterações de estado e atributos;
  9. Gameplay Tags devem comunicar estados sem criar dependências diretas entre sistemas;
  10. efeitos visuais não devem ser confundidos com a lógica autoritativa do combate.

O GAS possui uma curva de aprendizado considerável porque exige compreender várias camadas ao mesmo tempo.

Entretanto, essa divisão também é a sua principal vantagem.

Quando as responsabilidades estão bem definidas, torna-se possível adicionar novas mecânicas sem colocar todo o funcionamento dentro das classes dos personagens.

Próximos passos

A configuração atual ainda pode evoluir em diferentes pontos.

Um dos próximos objetivos é centralizar habilidades e efeitos iniciais em estruturas de configuração.

Em vez de manter referências individuais diretamente nas classes dos personagens, cada arquétipo poderia receber um conjunto contendo:

  • habilidades;
  • Gameplay Effects iniciais;
  • Attribute Sets relacionados;
  • níveis das habilidades;
  • tags adicionais.

Esse modelo se aproxima do conceito de Ability Sets utilizado em projetos maiores da Unreal Engine.

Também quero melhorar a comunicação entre o Ability System e a interface.

Atualmente, alguns Widget Controllers precisam encontrar o PlayerController, acessar o PlayerState, obter o ASC e registrar listeners para os atributos.

Esse fluxo funciona, mas cria dependências entre várias classes e assume que todos esses objetos já estão disponíveis.

Uma arquitetura melhor deve permitir que a interface receba explicitamente as referências necessárias quando o ASC estiver pronto, sem precisar procurar os objetos globalmente.

Outro ponto importante será revisar o fluxo de respawn e troca de Avatar. Como o ASC permanece no PlayerState, o sistema precisa atualizar corretamente suas referências quando um novo Character for criado.

O Gameplay Ability System não remove a complexidade de um sistema de combate.

Ele fornece ferramentas para distribuir essa complexidade entre componentes com responsabilidades mais claras.

No Oathbreakers, essa base será importante para continuar implementando ataques, parries, esquivas, efeitos, bosses e multiplayer sem transformar as classes dos personagens em um único bloco responsável por todo o jogo.

Continue explorando

Gostou deste estudo?

Confira outros artigos para acompanhar meus estudos em Unreal Engine, C++, Blueprints e gameplay programming.

Ver todos os artigos →