/tec: Dando contexto aos Campeões

Por: RiotAaronMike, Lucida

/tec é uma nova série de artigos que explora o lado técnico de League of Legends. Caso goste deste artigo, dê uma conferida no blog de Engenharia da Riot Games para saber ainda mais sobre os sistemas por trás do jogo.

Oi, galera! Aqui é RiotAaronMike e Lucida! Nós somos engenheiros de software da equipe de Mecânica de Jogo Principal do League e queremos falar um pouco sobre um sistema chamado Componente de Ações Contextuais (CAC), que acrescenta mais uma faceta às incríveis personalidades dos nossos Campeões. Basicamente, o CAC é um sistema que permite que os desenvolvedores agreguem interações customizadas aos Campeões, permitindo que esses Campeões reajam com mais naturalidade ao que acontece no Rift.


Explorando o sistema

Vamos começar vendo o que acontece quando a animação de provocação de Poppy é usada quando ela está sozinha e quando ela está ao lado do seu aliado de asas douradas, Galio:


Provocação da Poppy sem Galio

Provocação da Poppy na frente do Galio

Quando Poppy faz uma provocação quando está sozinha ou perto de Campeões que não têm relação com ela, sua fala de provocação será uma frase genérica. Quando Poppy está ao lado de seu aliado Galio, ela solta umas brincadeirinhas e, para completar, Galio responde se ainda estiver por perto quando ela terminar. Agora parece óbvio, mas quando League foi lançado em 2010, esse tipo de interação não era possível. A única forma de criar variações era deixar o mecanismo de áudio escolher uma fala aleatória a partir de uma lista. Isso obrigava os designers de som a usarem falas genéricas para evitar frases impróprias em determinadas situações.


Por exemplo, a fala de Lux "Rivalidade de irmãos! Que legal!" só faz sentido se ela estiver perto de Garen. Quando ela está perto de Katarina... Nem tanto. Uma fala dessas não seria possível no sistema antigo de League, pois o sistema não tinha como escolher essa fala corretamente. Estávamos perdedo ótimas oportunidades de exibir a personalidade de cada Campeão e a relação com outros Campeões!

E é exatamente para isso que o CAC serve. Ele é a base desses tipos de interação; é o que permite que Poppy e Lux estejam “contextualmente cientes" das informações em tempo real, como quem é o aliado mais próximo delas, qual item acabaram de comprar ou qual Campeão acabaram de abater. O CAC também é o que permite que Pulsefire Caitlyn tenha uma fala de pentakill exclusiva e que Xayah e Rakan fiquem paquerando no Rift.


Nos bastidores

O CAC foi desenvolvido com uma só finalidade: executar ações diferentes com base no contexto de qualquer situação específica do jogo. A estrutura do sistema pode ser representada como se segue:

  • Uma situação
    • Regra 1
      • Estados
      • Ação
    • Regra 2
      • Estados
      • Ação
    • Mais regras

Uma situação é uma construção predefinida do jogo, como abater Campeões (KillChampion), atacar estruturas (AttackBuilding) ou comprar um item (BuyItem). Nos últimos dois anos, nós incorporamos dezenas de situações em League. Cada situação pode conter uma lista de regras que, por sua vez, consistem em uma lista de estados e uma ação específica. Quando ocorre uma situação, uma regra cujos estados são atendidos é escolhida, e depois sua ação é executada. Um empate entre várias regras válidas pode ser resolvido considerando a ordem de chegada ou aleatoriamente, caso isso seja especificado. Os estados são os contextos predefinidos, como "quantidade de Vida própria", "nome do Campeão alvo", "região do mapa", "nível de habilidade" etc... As ações de voz podem escolher falas para ouvintes diferentes, entre eles o próprio Campeão, aliados, inimigos e espectadores.

Abaixo temos um exemplo de Camille com a skin Camille Cibernética provocando Ashe com a skin PROJETO: Ashe:


Assim como a maior parte da base de código do League, este sistema foi escrito em C++ e usa nosso Servidor de Dados do Jogo (GDS na sigla em inglês) em sua configuração. Dê uma olhada neste breve snippet de código que é executado quando um Campeão abate outro Campeão:


// To be called whenever a champion kills another champion
void HandleChampionKillSituation(Champion* killer, Champion* victim, 
  uint8_t killerMultikill = 0)
{
  ContextualActionComponent& cac = killer->GetContextualActionComponent();

  // See if the killer has a KillChampion situation
  const ContextualSituation* situation = cac.FindSituation(kKillChampion);
  if (situation != nullptr) {
    // Set the relevant facts
    ContextualFacts& facts = cac.GetFacts();
    facts.mKiller = killer;
    facts.mVictim = victim;
    facts.mKillerMultiKillSize = killerMultikill;

    // Attempt to find a rule that matches these kill facts
    const ContextualRule* rule = situation->PickRule(facts);
    if (rule) {  // a qualified rule has been found
      if (rule->ExecuteAudioAction(facts)) {
        // Tell the other CACs that the killer just executed this event
        cac.NotifyAllCacsOfPlayedAction(rule->GetAudioSituationTrigger());
      }

      // Reset the momentary facts
      facts.mKiller = nullptr;
      facts.mVictim = nullptr;
      facts.mKillerMultiKillSize = 0;
    }
  }
}

O snippet mostra como o sistema determina qual ação deve ser tomada de acordo com o contexto. A função PickRule (escolher regra) será repetida para todas as regras da situação KillChampion (abate de Campeão) até encontrar a regra que atende a todas as condições e depois executará a ação (ou ações) correspondente.


Autoria

As capturas de tela abaixo mostram uma regra que nós definimos para qualquer jogador habilidoso (ou sortudo) o bastante para conseguir um pentakill com Pulsefire Caitlyn:


Sempre que Pulsefire Caitlyn abate um Campeão inimigo, o CAC analisa as regras do estado KillChampion. Essa regra diz: se este for o quinto abate de uma série de abates, execute a fala KillChampion3DPentakill para si mesmo (o jogador) e para os inimigos do jogador. Observe que esta regra tem um limite de 3 “occorências" — observe que está escrito errado (volto já, vou corrigir) — então ela será executada para os primeiros três pentakills pois, como sabemos, no quarto as coisas ficam um pouco barulhentas.


Vitórias

Antigamente, o áudio era acionado diretamente por eventos em vários sistemas do jogo. Os eventos podiam ser a criação de uma partícula, eventos de animação, conjuração de habilidades, comandos do usuário etc... Por exemplo, quando o jogador movimenta seu próprio Campeão, o cliente do jogo cria um evento de áudio com um nome tipo "Champion_VO_MoveCommand" e tenta executar o clipe de áudio correspondente. Como os gatilhos antigos não tinham conhecimento do contexto do jogo, eles não conseguiam realizar interações customizadas.

Os eventos diretos são apenas a ponta do iceberg do que pode ser feito usando o CAC. A combinação de situações e regras permite que interações extremamente específicas sejam criadas. Antes desse sistema, nós tínhamos algumas interações específicas no jogo, mas contávamos com a aleatoriedade para aplicá-las com uma frequência apropriada. Por exemplo, Zac tem duas provocações genéricas: "Fique monstro, ou vá pra casa" e "O importante não é quanto você levanta, mas quão bonito você fica." Quando ele faz uma provocação, o jogo escolhe aleatoriamente uma das falas. Agora nós temos ajustes disponíveis para que as falas só apareçam nas situações apropriadas. Desta forma, podemos causar interações raras e detalhadas de propósito em vez de deixar tudo às custas da sorte.


Xayah e Rakan

No início de 2016, decidimos criar o primeiro casal de Campeões do universo de League of Legends. Nosso objetivo era fazer esses dois Campeões interagirem no jogo como um casal em vez de usar apenas interações genéricas e permutáveis. E se quiséssemos que Rakan erguesse Xayah durante uma dança? E se quiséssemos que Xayah cutucasse Rakan com umas brincadeirinhas (carinhosas)? E se Rakan precisasse alertar Xayah sobre algum perigo iminente?

Para transformar esses "e se" em possibilidades, nós tínhamos que acrescentar algumas ações e situações novas ao CAC.


Xayah e Rakan


Animação

O sistema de animação não tinha acesso a parte do contexto necessário para que os animadores criassem a desejada dança sincronizada ou as cenas de retorno. Para alcançar esse resultado final, adicionamos um novo tipo de ação ao CAC para controlar as animações dos Campeões. Sempre que Xayah faz alguma coisa — ou nada, caso esteja ociosa — ela envia para o sistema de animação uma solicitação de PlayAnimation (reproduzir animação) com o nome da animação desejada. Alteramos esse fluxo para que o CAC intercepte essas solicitações e verifique se alguma condição contextual é atendida. Caso positivo, a animação é substituída por uma animação mais adequada ao contexto. Em seguida, a solicitação é enviada para ser executada feliz da vida pelo sistema de animação.


CACs interativos

O próximo desafio era a dança. Como Xayah e Rakan avisariam um ao outro que deveriam dançar juntos? Conseguimos isso adicionando uma nova situação que é acionada sempre que outro Campeão realiza uma ação do CAC. Todos os CACs do jogo recebem uma notificação de que a ação foi concluída, juntamente com o contexto atual do jogo para que possam determinar se uma reação é necessária.


Da esquerda para a direita: Os dois estão ociosos, Xayah começa a dançar sozinha, Rakan entra na dança, e eles começam a dançar em sincronia.


Sinais de alerta contextuais

Outra grande vitória foi conseguir reecaminhar as solicitações de sinal de alerta pelo CAC. Agora, além dos sinais de alerta regulares, os amantes conseguem dizer coisas do tipo "Amor, cuidado!" para o sinal de alerta Perigo; e "Eles não estão aqui!" para o sinal de alerta Inimigo Desaparecido.


Questões técnicas


Anti-trapaça

Trapaças e hackeamentos são preocupações cruciais sempre que adicionamos novos sistemas como o CAC ao League of Legends. Uma forma de hackear envolve extrair mais informações do que o jogo oferece para que os jogadores tenham uma vantagem competitiva. Uma trapaceiro pode explorar um sistema contextual para fornecer estas informações. Imagine se Elise acionasse uma fala como "Meus sentidos aracnídeos estão aguçados…" sempre que sua equipe estivesse escondida em um arbusto próximo. Para evitar esses oportunismos, programamos o CAC para considerar somente as informações que o cliente já tem e nada mais (em outras palavras, ele vê o que você vê).


Desempenho

Queremos dar aos desenvolvedores a liberdade e a usabilidade que eles precisam para dar vida aos personagens de League, mas também queremos evitar reduções no desempenho de qualquer jogador, independentemente de ele estar usando uma máquina colossal com cooler de nitrogênio líquido ou um laptop basicão. Nossa meta sempre foi deixar o sistema o mais leve possível e com o melhor desempenho possível em todas as etapas do processo. Uma combinação de escolhas de programação e práticas recomendadas nos permitem alcançar isso:

  1. As situações são armazenadas em um hashmap com uma string de hash como tipo chave. Com essa estrutura, podemos rapidamente recuperar uma situação de um objeto do CAC. Se um Campeão não possuir dados para determinada situação, a função handle será simplesmente retornada. Como todos os Campeões têm um pequeno conjunto de situações relevantes, a maioria das situações custam muito pouco.
  2. Preferimos situações específicas do que situações genéricas. O normal seria preferirmos situações genéricas e reutilizáveis que pudessem resolver vários problemas de uma vez, mas este caso é diferente. Situações mais genéricas possuem mais regras e cada uma dessas regras contêm outras condições que devem ser processadas pela CPU. Transformar uma situação genérica em algumas poucas situações específicas reduz o número de regras e melhora o desempenho. Situações sem regras podem até gerar retornos diretos. Por exemplo, nós temos quatro situações específicas de abate: KillChampion (abate de Campeão), KillTurret (abate de torre), KillNeutralMinion (abate de monstro neutro e tropas) e KillWard (abate de sentinela). KillChampion é a que normalmente tem mais variações, mas só acontece poucas vezes em uma partida. KillNeutralMinion é a que tem menos variações, mas acontece com mais frequência. Se usássemos uma situação genérica como KillTarget (abate de um alvo) para todas as situações de abate, teríamos que analisar uma enorme lista de regras sempre que um desses quatro tipos de alvo fosse abatido.
  3. É preferível verificar fatos ou condições importantes, porém simples, primeiro. Se alguma dessas condições falhar, outras verificações complexas do processo podem ser ignoradas.
    1. Ignore situações de áudio se o proprietário estiver falando. League não permite a execução de falas simultâneas do mesmo personagem. Esta é uma ótima oportunidade de otimização. Se o CAC identificar que o proprietário está falando, ele pode ignorar novas situações de áudio. No caso do spam de animações, o CAC ainda é muito eficiente, pois ignora a maioria dos processos depois que o Campeão começa a falar.
    2. Outra condição importante é a situação do Tempo de Recarga. Se o Campeão não responder a uma situação pouco após a última execução, então não há motivo para processar a situação.
  4. Se possível, evite situações de alta frequência. Caso uma situação ocorra com frequência, há algumas maneiras de evitar uma redução de desempenho:
    1. Defina um Tempo de Recarga para a situação, assim o cliente do jogo não precisará verificar a situação sempre que ela acontecer.
    2. As situações de alta frequência devem ter poucas regras para que sejam executadas mais rápido.

Conclusão

Com o CAC, sistemas como o de áudio e o de animação ficam mais por dentro do contexto do jogo, abrindo um mundo de possibilidades para a criatividade de nossos colegas. Assim eles têm condições de aprimorar a personalidade dos Campeões e continuar expandindo o mundo de League of Legends. Cada uma das falas que adicionamos ao jogo é uma oportunidade de fazer alguém sorrir e aproximar os jogadores de seus Campeões favoritos.



4 months ago