/tech: kontekst dla bohaterów

po RiotAaronMike, Lucida

/tech to nowa seria artykułów prezentująca technologiczne aspekty League of Legends. Jeśli podoba wam się ta historia, zajrzyjcie na bloga inżynierii Riot Games (po angielsku), gdzie znajdziecie jeszcze bardziej szczegółowe omówienia systemów wykorzystywanych w grze.

Cześć, przyjaciele. Tu AaronMike i Lucida! Jesteśmy inżynierami oprogramowania z zespołu ds. rozgrywki League i chcemy opowiedzieć o systemie zwanym Kontekstowym Składnikiem Akcji (Contextual Action Component — CAC w skrócie), który dodaje osobowościom naszych bohaterów jeszcze więcej połysku. W skrócie CAC to system, który umożliwia deweloperom wyposażanie bohaterów w niestandardowe interakcje, dzięki którym ci bohaterowie reagują naturalniej na to, co dzieje się na Summoner’s Rift.


Eksploracja systemu

Zacznijmy od przyjrzenia się przykładowi, co się dzieje, gdy Poppy używa swojej emotki prowokacji, jeśli jest sama, a także w przypadku, gdy stoi obok swojego złotoskrzydłego sojusznika, Galio:


Prowokacja Poppy bez Galio

Prowokacja Poppy naprzeciw Galio

Gdy Poppy prowokuje sama lub w pobliżu niepowiązanych z nią bohaterów, jej dialog prowokacji będzie jednym z kilku tekstów ogólnych. Jednak gdy Poppy stoi w pobliżu sojuszniczego Galio, zaczyna się z nim przekomarzać, a co więcej, Galio odpowie, jeśli wciąż będzie blisko, gdy Poppy skończy. Teraz wydaje się to oczywiste, ale gdy League wyszło w 2010 roku, tego rodzaju interakcje nie były możliwe. Jedyny sposób wprowadzenia urozmaicenia polegał na tym, by pozwolić silnikowi dźwięku na wybieranie losowego dialogu z listy. To wymuszało na inżynierach dźwięku stosowanie neutralnych, ogólnych dialogów dla uniknięcia odtwarzania niepasujących głosów w pewnych sytuacjach.


Na przykład wypowiadana przez Lux kwestia: „Rywalizacja między rodzeństwem! Będzie zabawnie!” ma sens, gdy stoi ona obok Garena. A gdy stoi obok Katariny? Nie za bardzo. Taki dialog nie byłby dopuszczalny w starym systemie audio League, ponieważ system nie miał możliwości prawidłowego wybrania momentu odtworzenia dialogu. Traciliśmy cenne okazje umożliwiające pokazanie osobowości każdego z bohaterów oraz ich związków z innymi bohaterami!

I właśnie do tego służy CAC. Stanowi esencję interakcji tego typu; jest tym, co sprawia, że Poppy i Lux są „świadomi kontekstu” pobieranych w czasie rzeczywistym informacji, takich jak ich najbliższy sojusznik, kupiony przedmiot albo bohater, którego właśnie udało im się zabić. Ponadto udostępnia Cybernetycznej Caitlyn jej unikalny dialog Pentakill oraz pozwala Xayah i Rakanowi szczebiotać do siebie na Summoner’s Rift.


Pod maską

CAC został zaprojektowany z myślą o prostej obsłudze: ma wykonywać różne akcje w oparciu o kontekst danej sytuacji w grze. Strukturę systemu można przedstawić następująco:

  • - Sytuacja
    • - Zasada 1
      • - Warunki
      • - Akcja
    • - Zasada 2
      • - Warunki
      • - Akcja
    • - Więcej zasad

Sytuacja to predefiniowany konstrukt w naszej grze, taki jak KillChampion (zabij bohatera), AttackBuilding (zaatakuj budowlę) lub BuyItem (kup przedmiot). W toku kilku ostatnich lat wbudowaliśmy do League dziesiątki sytuacji. Każda sytuacja może zawierać listę zasad, która sama zawiera listę warunków oraz jedną akcję. Gdy następuje jakaś sytuacja, wybierana jest zasada, której warunki są spełnione, a następnie wykonywana jest jej akcja. Remis między różnymi pasującymi zasadami może zostać rozstrzygnięty według kolejności ich wystąpienia lub losowo, jeśli tak ustalono. Warunki to predefiniowane konteksty, takie jak „self HP range” (zakres własnego zdrowia), „target champion name” (nazwa wskazanego bohatera), „map region” (region mapy), „spell level” (poziom czaru) itp. Akcje odtworzenia głosu mogą wybierać dialogi przeznaczone dla różnych słuchaczy takich jak (między innymi) gracz, sojusznicy, wrogowie i obserwatorzy.

Poniżej przykład Camille ze skórką Cyber Camille, prowokującej Ashe ze skórką PROJEKT: Ashe:


Podobnie jak większość podstawowego kodu League, ten system napisany jest w języku C++ i wykorzystuje w swojej konfiguracji nasz serwer danych gry (Game Data Server — GDS). Przyjrzyjcie się załączonemu fragmentowi kodu, który jest wywoływany za każdym razem, gdy bohater zabije innego bohatera:


// 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;
    }
  }
}

Ten fragment kodu pokazuje, w jaki sposób system ustala, którą akcję należy wykonać w zależności od kontekstu. Funkcja PickRule (wybierz zasadę) będzie przetwarzała każdą z zasad sytuacji KillChampion, aż znajdzie zasadę, która spełnia wszystkie warunki, a następnie wykona odpowiednią akcję (lub akcje).


Przetwarzanie

Poniższy zrzut ekranu pokazuje zasadę, którą ustawiliśmy dla każdego gracza, któremu uda się zręcznie (lub fartem) zdobyć pentakilla z Cybernetyczną Caitlyn:


Za każdym razem, gdy Cybernetyczna Caitlyn zabije wrogiego bohatera, CAC przejrzy wszystkie zasady w sytuacji KillChampion. Ta zasada mówi: jeśli to jest piąte zabójstwo w serii, odtwórz dialog KillChampion3DPentakill słyszalny przez gracza i jego wrogów. Zauważcie, że ta zasada ma ograniczenie do 3 „wystąpień”, zatem będzie odtwarzana tylko podczas pierwszych trzech udanych pentakillów, ponieważ, jak wszyscy wiemy, przy czwartym robi się dość hałaśliwie.


Sukcesy

Wcześniej w różnych systemach w całej grze dźwięk był uruchamiany bezpośrednio przez wydarzenia. Przykłady wydarzeń to tworzenie efektów cząsteczkowych, animacje, rzucanie czarów, polecenie od użytkownika itd. Na przykład gdy gracz porusza swojego bohatera, klient gry tworzy wydarzenie audio o nazwie brzmiącej jak „Champion_VO_MoveCommand” i próbuje odtworzyć odpowiedni klip dźwiękowy. Ponieważ dawne przełączniki nie znały kontekstu w grze, nie umożliwiały elastycznych interakcji.

Wydarzenia bezpośrednie stanowią tylko niewielki fragment tego, co można osiągnąć z użyciem CAC-a. Kombinacja sytuacji i zasad umożliwia przetwarzanie bardzo wyspecjalizowanych interakcji. Przed tym systemem mieliśmy w grze kilka wyspecjalizowanych interakcji, ale polegały one na ich losowym wykonywaniu z odpowiednią częstością. Na przykład Zac ma dwa ogólne dialogi prowokacji: „Daj z siebie wszystko lub wracaj do domu” i „Nie chodzi o to, ile dźwigasz. Chodzi o to, jak wyglądasz”. Gdy rzuca prowokację, gra losowo wybiera jeden z dialogów do odtworzenia. Teraz możemy dostosowywać te dialogi, by pojawiały się tylko w odpowiednich sytuacjach. W ten sposób dysponujemy rzadkimi, szczegółowymi interakcjami pojawiającymi się w konkretnych sytuacjach zamiast losowo.


Xayah i Rakan

Na początku 2016 roku zdecydowaliśmy się stworzyć naszą pierwszą parę bohaterów w uniwersum League of Legends. Naszym celem było sprawienie, by tych dwoje bohaterów dokonywało interakcji w grze jak prawdziwi kochankowie, zamiast wykorzystywać tylko ogólne i wymienialne interakcje. A gdybyśmy chcieli, by Rakan mógł unosić Xayah w powietrze podczas tańca? Albo żeby Xayah przekomarzała się (słodko) z Rakanem? A gdyby Rakan musiał ostrzec Xayah o nadchodzącym zagrożeniu?

Chcąc zrealizować te zamierzenia, musieliśmy uaktualnić CAC-a o kilka nowych akcji i sytuacji.


Xayah i Rakan


Animacja

System animacji nie był wyposażony w pełny kontekst niezbędny animatorom do stworzenia odpowiedniego tańca zsynchronizowanego lub animacji powrotów. Aby osiągnąć efekt końcowy, dodaliśmy do CAC-a nowy typ akcji, umożliwiający kontrolowanie animacji bohaterów. Za każdym razem gdy Xayah robi cokolwiek — albo nie robi nic, jeśli pozostaje bezczynna — wysyła do systemu animacji polecenie PlayAnimation (odtwórz animację) wraz z nazwą żądanej animacji. Zmodyfikowaliśmy ten proces, aby CAC interpretował takie polecenia i sprawdzał, czy spełnione są jakieś warunki kontekstowe. Jeśli istnieją spełnione warunki, standardowa animacja zostaje wymieniona na animację bardziej pasującą do danego kontekstu. Następnie polecenie jest szczęśliwie wysyłane do systemu animacji.


Interaktywne CAC-i

Następnym wyzwaniem był taniec. Jak Xayah i Rakan mogą dać sobie nawzajem znać, że chcą zacząć tańczyć ze sobą? Osiągnięto to przez dodanie nowej sytuacji, która uruchamia się za każdym razem, gdy inny bohater wykona akcję CAC. Wszystkie CAC-i w grze zostają powiadomione o wykonanej akcji, włącznie z aktualnym kontekstem w grze, dzięki czemu mogą ustalić, czy potrzebna jest reakcja.


Od lewej do prawej: oboje stoją bezczynnie, Xayah zaczyna tańczyć sama, Rakan dołącza do tańca i wykonują taniec zsynchronizowany.


Sygnały kontekstowe

Kolejnym niesamowitym sukcesem było przepuszczenie przez CAC-a poleceń sygnałów. Teraz oprócz zwykłych sygnałów kochankowie mogą mówić takie rzeczy jak „Skarbie, uważaj!” w przypadku sygnału Niebezpieczeństwo lub „Nie ma ich tutaj!” w przypadku sygnału Brakuje wroga.


Problemy techniczne


Zapobieganie oszukiwaniu

Oszukiwanie i hackowanie to kluczowe problemy, które bierzemy pod uwagę, dodając do League of Legends nowe systemy, takie jak CAC. Jedna z form hackowania polega na osiąganiu przewagi dzięki uzyskiwaniu dodatkowych informacji, wykraczających poza te, które gra dostarcza graczom. Oszust mógłby uzyskiwać te informacje, wykorzystując odpowiednio system kontekstów. Wyobraźcie sobie, że Elise wygłasza dialog w rodzaju: „Moje pajęcze zmysły dają mi znać...”, za każdym razem, gdy w pobliskich krzakach ukrywa się wasza drużyna. Aby zapobiec tego rodzaju nadużyciom, zaprojektowaliśmy CAC-a w ten sposób, by działał tylko w oparciu o informacje, które klient gry już dostał, i o nic więcej (innymi słowy, system widzi to, co widzicie wy).


Wydajność

Chcieliśmy zapewnić deweloperom swobodę pracy, której potrzebują, by ożywiać bohaterów League, ale chcieliśmy także uniknąć spadku płynności działania u każdego z graczy, niezależnie od tego, czy dany gracz używa chłodzonego płynnym azotem monstrum, czy słabego laptopa. Naszym celem zawsze było zadbanie, by system był możliwie jak najlżejszy i jak najwydajniejszy na każdym etapie procesu. Osiągnięcie tego celu jest możliwe dzięki połączeniu dobrego stylu kodowania i najlepszych praktyk programistycznych:

  1. Sytuacje przechowywane są w mapie etykiet przeszukiwanej według ciągów etykiet. W tej strukturze możemy szybko wyizolować sytuację związaną z obiektem CAC. Jeśli bohater nie ma danych odnoszących się do danej sytuacji, funkcja po prostu nie zwróci żadnego wyniku. Ponieważ każdy z bohaterów ma niewielki zbiór istotnych dla siebie sytuacji, większość z nich oznacza bardzo niewielkie obciążenie.
  2. Sytuacje szczególne są bardziej preferowane niż ogólne. Normalnie wolelibyśmy mieć ogólne, możliwe do ponownego wykorzystania rozwiązania, które jednocześnie rozwiązują wiele problemów, ale to jest przypadek szczególny. Ogólniejsze sytuacje zawierają więcej zasad, z których każda zawiera inne warunki, które musi przetworzyć procesor. Podzielenie sytuacji ogólnej na kilka sytuacji szczególnych zmniejsza liczbę zasad i poprawia wydajność. Sytuacje bez zasad mogą być realizowane bezpośrednio. Na przykład mamy cztery konkretne sytuacje zabójstwa: KillChampion, KillTurret, KillNeutralMinion i KillWard (kolejno: zabicie bohatera, wieży, neutralnego stwora i totemu). KillChampion zwykle ma najwięcej wariacji, ale zdarza się tylko kilka razy w jednej grze. KillNeutralMinion ma najmniej wariacji, ale zdarza się najczęściej. Gdybyśmy zamiast tego użyli ogólnej sytuacji takiej jak KillTarget (zabicie celu) dla wszystkich naszych sytuacji zabójstwa, musielibyśmy przetworzyć wielką listę zasad za każdym razem, gdy zabity zostanie jeden z tych czterech typów celów.
  3. Wolimy najpierw sprawdzać proste, ale ważne fakty lub warunki. Jeśli którykolwiek z tych warunków nie zostanie znaleziony, pozostałe złożone testy w tym procesie mogą zostać pominięte.
    1. Pomijamy odtwarzanie nowych kwestii bohatera, który wygłasza już jakiś dialog. League nie pozwala na jednoczesne odtwarzanie przez jedną postać kilku dialogów. To zapewnia nam świetne możliwości optymalizacji. Jeśli CAC stwierdzi, że postać coś mówi, może zignorować nowe sytuacje audio. W przypadku spamowania emotek CAC jest bardzo wydajny, ponieważ pominie większość procesu, gdy tylko bohater zacznie mówić.
    2. Kolejnym ważnym warunkiem jest czas odnowienia sytuacji. Jeśli bohater nie powinien zareagować na sytuację w krótkim czasie po ostatniej akcji, nie ma konieczności przetworzenia tej sytuacji.
  4. W miarę możliwości unikamy sytuacji wydarzających się bardzo często. Jeśli jakaś sytuacja wydarza się często, istnieje kilka sposobów na uniknięcie spadku wydajności:
    1. Ustawienie czasu odnowienia dla sytuacji, aby klient gry mógł pominąć sprawdzanie za każdym razem, gdy wydarzy się ta sytuacja.
    2. Upewnienie się, by częste sytuacje miały niewiele zasad, co zapewni ich szybsze przetwarzanie.

Podsumowanie

Dzięki CAC-owi systemy takie jak audio i animacje są bardziej świadome kontekstu w grze, co zapewnia więcej nowych możliwości dla naszych kreatywnych współpracowników. To umożliwia im wzbogacanie osobowości bohaterów i dalsze rozbudowywanie świata League of Legends. Każdy dialog, który dodajemy do gry, to okazja, by wywołać uśmiech na czyjejś twarzy i jeszcze bardziej przybliżyć graczy do ich ulubionych bohaterów.



11 months ago