Za kulisami Warcraft Rumble: Obliczanie doświadczenia mini
Witajcie, rozrabiaki!
Jestem Andy Lim, główny inżynier ds. funkcji serwera w Warcraft Rumble. Zespół ds. serwerów jest odpowiedzialny za wszystko, czego moglibyście się spodziewać, w tym za łączność sieciową, przetwarzanie w chmurze oraz przechowywanie danych. Tworzymy również funkcje gry, takie jak postępy kampanii czy zadania. Chciałbym pokazać kulisy i opowiedzieć o tym, jak przechowujemy doświadczenie, aby następnie wykorzystać je do obliczania poziomów dla każdej mini.
Poznajcie Cassandrę
Uwaga! Przed wami szczegóły techniczne – mogą wydać się skomplikowane, jednak zachęcamy do lektury.
Zaczniemy od omówienia Cassandry – naszego rozwiązania do przechowywania danych, które wykorzystujemy, aby śledzić częste zmiany danych naszych graczy dla wielu użytkowników aktywnych jednocześnie. Cassandra to darmowa, popularna, wysoce skalowalna, rozproszona baza danych, która zapewnia odpowiedni kompromis pomiędzy spójnością a dostępnością danych. Jeśli chodzi o same dane, Cassandra świetnie radzi sobie z wielkimi zbiorami danych, bez konieczności wymuszania twardych schematów. Stworzyliśmy narzędzie, które umożliwia inżynierom definiowanie tabel naszej bazy danych i schematów tabel, dostosowując je do potrzeb poszczególnych funkcji. Zapewnia to elastyczność w zakresie strukturyzowania i organizacji. Możemy z łatwością zapisywać i weryfikować nasze schematy i kwerendy.
Czym jest schemat? Schemat bazy danych określa, w jaki sposób dane są zorganizowane w relacyjnej bazie danych, czyli na przykład to, co można znaleźć w tabelach.
Przechowywanie doświadczenia w formie rejestru
Cassandra jest znana z tego, że potrafi zapisywać dane niezwykle szybko, ale wolno je odczytuje. Aktualizowanie danych w miejscu często polega na przeprowadzeniu operacji odczytu, a następnie zapisu, co również jest powolne. Aby obejść ten problem, przechowujemy dane naszych graczy w formie rejestru. W rejestrze każda linia jest zapisywana jako zmiana, a jej odczytanie równa się odczytaniu wszystkich wpisów i przeprowadzeniu na nich obliczeń.
Dobrym przykładem rejestru jest karta kredytowa. Każda transakcja jest zapisywana jako pojedynczy wpis o wartości dodatniej lub ujemnej. Za każdym razem, gdy karta zostaje użyta, występuje transakcja o ujemnej wartości. Za każdym razem, gdy ją opłacasz, występuje transakcja o dodatniej wartości. Skąd wiadomo, ile należy opłacić? Trzeba przejrzeć cały rejestr i dodać/odjąć każdą wartość.
Wystarczy pomyśleć o danych w miejscu, jak o jednej przechowywanej rzeczy. Za każdym razem, gdy chcecie zmienić wartość, musicie ją odczytać, dokonać zmiany, a następnie zapisać nową wartość. Na przykładzie tabeli wyników meczu piłki nożnej – za każdym razem, gdy drużyna strzeli gola, należy odczytać dane, aby dowiedzieć się, jaki jest wynik całkowity, dodać strzeloną bramkę, a następnie zapisać wartość z powrotem na tabeli wyników.
Konkretnym przykładem w Arclight jest rejestr doświadczenia dla każdego mini. Po każdej misji dla jednego mini zostanie dodany wiersz, który będzie wskazywał ilość zdobytego doświadczenia. Po 5 misjach możecie mieć wpisy, które wyglądają w ten sposób:
Tabela 1
Gracz |
Mini |
Wartość |
Czas |
---|---|---|---|
Andy |
Gnol wojownik |
3 |
Poniedziałek, 14:00 |
Andy |
Gryfi jeździec |
3 |
Poniedziałek, 14:05 |
Andy |
Gnol wojownik |
3 |
Poniedziałek, 14:10 |
Andy |
Pilotka O.P.O.R. |
3 |
Poniedziałek, 14:15 |
Andy |
Gnol wojownik |
3 |
Poniedziałek, 14:20 |
W celu uzyskania całkowitego doświadczenia danego mini pozyskujemy wszystkie wpisy rejestru, grupujemy je według każdego mini i dodajemy wartości doświadczenia. Oto rezultat:
Tabela 2
Gracz |
Mini |
Suma |
---|---|---|
Andy |
Gnol wojownik |
9 |
Andy |
Gryfi jeździec |
3 |
Andy |
Pilotka O.P.O.R. |
3 |
Tworzenie zestawień
Omówiliśmy już, w jaki sposób przechowujemy doświadczenie. Pora przyjrzeć się, jak możemy dopracować obliczenia, które muszą zostać wykonane dla każdego mini. Przechowywanie poszczególnych wierszy dla każdej instancji uzyskania doświadczenia przez mini oznaczałoby, że ich odczyt byłby zbyt skomplikowany. Istniałoby tak dużo wierszy, że tabela byłaby zapełniana w nieskończoność… jest jednak ALE. Powiedzmy, że zwyczajny dzień z Rumble to połączenie 15 zadań lub gier PVP oraz 20 Wezbrań, za które zdobywacie monety. Należy też dodać cotygodniową wyprawę do podziemi celem wzmocnienia armii. Oznacza to 100 wpisów tygodniowo. Po 3 miesiącach gry ilość wpisów w rejestrze wynosiłaby jakieś 1260. Obliczenie wyniku całkowitego oznaczałoby pozyskanie ich poprzez system Cassandry, co wiąże się z powolnym odczytem. Auć.
Właśnie po to zaprojektowaliśmy rozwiązanie: zestawienia. Zestawienie obejmuje obliczenie wartości do pewnego punktu w czasie. W tym przypadku sumujemy je ze sobą, przedstawiamy w jednym wierszu i przechowujemy w drugiej tabeli. Teraz wiecie, do czego przydaje się znacznik czasowy. Jeśli chodzi o doświadczenie, to najważniejszymi pozycjami są gracz i mini, a kalkulacja jest sumą. Sporządźmy zestawienie dla wszystkich wpisów z rejestru do wtorku, godz. 00:00 i przechowajmy wyniki w oddzielnej tabeli Cassandry. Wpisy mogą wyglądać następująco:
Tabela 3
Gracz |
Mini |
Suma |
Data końcowa |
---|---|---|---|
Andy |
Gnol wojownik |
9 |
Wtorek, 00:00 |
Andy |
Gryfi jeździec |
3 |
Wtorek, 00:00 |
Andy |
Pilotka O.P.O.R. |
3 |
Wtorek, 00:00 |
Rozgrywacie więcej gier w środę i generujecie więcej wpisów doświadczenia. Pierwotna tabela z doświadczeniem byłaby bardziej zapełniona.
Tabela 4
Gracz |
Mini |
Wartość |
Czas |
---|---|---|---|
Andy |
Gnol wojownik |
3 |
Poniedziałek, 14:00 |
Andy |
Gryfi jeździec |
3 |
Poniedziałek, 14:05 |
Andy |
Gnol wojownik |
3 |
Poniedziałek, 14:10 |
Andy |
Pilotka O.P.O.R. |
3 |
Poniedziałek, 14:15 |
Andy |
Gnol wojownik |
3 |
Poniedziałek, 14:20 |
Andy |
Pilotka O.P.O.R. |
3 |
Środa, 12:00 |
Andy |
Łańcuch Błyskawic |
3 |
Środa, 12:05 |
Andy |
Gryfi jeździec |
3 |
Środa, 12:10 |
Teraz chcemy przeprowadzić pełny przegląd i ponownie obliczyć poziomy mini po rozegraniu tych gier. Kwerendujemy obie tabele – tabelę zestawień o jeden wpis, a rejestr o wpisy po konkretnym punkcie w czasie – a następnie sumujemy wartości, co daje nam tabelę z łącznym doświadczeniem. Odczytujemy więc tabelę 3, kwerendujemy tabelę 4 o dane, które istnieją tylko po każdym wierszu z tabeli 3. Poniżej lista kroków:
- Odczyt wiersza z tabeli 3
Gryfi jeździec |
3 |
Wtorek, 00:00 |
- Odczyt wierszy z tabeli 3 dla Gryfiego jeźdźca po wtorku, godz. 00:00
Gryfi jeździec |
3 |
Środa, 12:10 |
- Teraz sumujemy oba zestawy danych, aby wygenerować łączną wartość:
Gryfi jeździec |
6 |
Teraz widzicie, że takie podejście uprościłoby niektóre odczyty z tabeli 4.
Możecie pomyśleć, że powinniśmy przechowywać nowo obliczone dane po zapisaniu nowego wpisu rejestru. Może to wyglądać na dobre rozwiązanie w teorii, ale poskutkowałoby brakiem spójności danych i degradacją wydajności. Przykładem może być fakt, że system byłby zajęty ponownym obliczaniem i przechowywaniem danych ze względu na potrzebę odczytu, a następnie zapisu. Musimy zrównoważyć proces obliczania podczas gry, by zapewnić odpowiednią wydajność wszystkim graczom.
Przykładem niespójnych danych są obliczenia na koniec gry. Infrastruktura naszego serwera i wsparcie platformy do obsługi spóźnionych wiadomości. Załóżmy, że istnieje przejściowy problem w dwóch centrach danych (A i B), a wiadomość dotycząca przyznania graczowi doświadczenia mini zostaje wysłana z A, ale nie dotarła jeszcze do B. Scenariusz wyglądałby następująco: gracie chwilę przed północą. Wygrywacie w środę o godz. 23:59. Procesor zestawiający był skonfigurowany tak, by pracować codziennie dla danych do bieżącego dnia. Rozpocznie o północy, przetworzy wartości całkowitego doświadczenia do czwartku, godz. 00:00. Dane zostają zapisane w drugiej tabeli, a nowe odczyty doświadczenia wyszukiwałyby ostatnią zachowaną wartość i sumowałyby wszystkie po czwartku, godz. 00:00. Spóźniona wiadomość zostaje dostarczona, ale gra była ukończona w środę o godz. 23:59, co spowoduje zapisanie wpisu z doświadczeniem z taką właśnie godziną. Tabela zestawień będzie zawierać ten wpis, a zapytanie o późniejsze dane nie pokaże tego wpisu. Niekompletne dane stanowią problem. Co z wiadomościami, które przyjdą kilka dni później? Mamy specjalny system monitorowania i powiadamiania, który niemal zawsze gwarantuje, że nowe informacje zostaną przetworzone w odpowiednim czasie przed następnym zestawieniem.
Obliczanie poziomów mini
Spójrzmy na tabelę całkowitego doświadczenia. Zauważcie, że nie ma tam obliczeń dotyczących poziomów. Bierzemy tylko sumę uzyskanego doświadczenia. Oddzielnie występuje tabela statyczna, którą twórcy gry zaprojektowali w taki sposób. Mini zaczyna na poziomie 1, po zyskaniu 1 PD otrzyma poziom 2, a potem musi zdobyć 3 PD, by awansować na poziom 3.
Poziom |
Liczba do następnego poziomu |
---|---|
1 |
1 |
2 |
3 |
3 |
6 |
4 |
10 |
5 |
20 |
… |
… |
10 |
250 |
Połączenie dwóch zestawień pozwala obliczyć faktyczny poziom mini w czasie rzeczywistym, obliczając sumę bieżącą, co daje nam ostateczne dane:
Mini |
Poziom |
PD |
Liczba do następnego poziomu |
---|---|---|---|
Gnol wojownik |
3 |
5 |
1 |
Gryfi jeździec |
3 |
2 |
4 |
Pilotka O.P.O.R. |
3 |
2 |
4 |
Łańcuch Błyskawic |
2 |
2 |
1 |
Twórcy gry mogą dowolnie zmieniać ilość doświadczenia, jaka jest niezbędna do awansowania na wyższy poziom. Mogą zadecydować, że za trudno jest awansować na niższych poziomach i zmniejszyć potrzebne PD. Oznacza to, że liczba PD mini na ich obecnym poziomie zostałaby nieco zwiększona i potrzebowałyby mniej, aby osiągnąć następny. Wszystko bez zmian wprowadzonych w danych graczy zapisanych w bazie danych. Działa to również w drugą stronę. Twórcy gry mogą stwierdzić, że PD zyskuje się za szybko. Nie będziemy cofać poziomów mini do tych sprzed zmiany. Możliwe jest jednak wprowadzenie rozwiązania, dzięki któremu będziemy mogli przyznawać nieco doświadczenia dla mini.
Alternatywne rozwiązania
Oprócz obecnego rozwiązania rozpatrywaliśmy różne podejścia do przechowywania uzyskiwanego doświadczenia i obliczania poziomu dla mini. Niektóre oznaczałyby dłuższe przestoje w celu zmiany doświadczenia graczy, a inne świetnie sprawdzały się w zapewnianiu dobrych doświadczeń z gry i nadążaniu z postępami graczy.
„Zawsze przechowuj poziom i PD mini do następnego poziomu”
A gdyby tak wykorzystać standardowy wzorzec SQL i użyć aktualizacji transakcyjnych? Pozwalają one zaktualizować dane w całości – wszystkie albo żadne. Jeśli jakaś część się nie zapisze, wszystkie wartości zostaną cofnięte do pierwotnej wartości. Zapewniają one zbiór właściwości A.C.I.D. – niepodzielność, spójność, izolację i trwałość.
Wykorzystują jeden wiersz, aby zachować doświadczenie każdego mini i całkowity poziom, dzięki czemu odczyt nie stanowi dużego obciążenia.
Gdy każda mini otrzymuje doświadczenie, jest aktualizowana do 2 PD z 8 PD do awansu na poziom 3. Taki rodzaj zmiany zmusza odczyt danych, ponowne obliczenie i zapis do bazy danych. Wzorzec ten nie sprawdza się z Cassandrą i jej silnymi cechami. Kolejnym problemem z takim podejściem są zmieniające się ilości potrzebne do następnego poziomu, które wymagałyby wyłączenie dostępu graczom do gry, abyśmy mogli zmodyfikować dane wszystkich mini dla graczy.
„Przechowywanie w formie procentowej”
Zastanawialiśmy się również nad przechowywaniem procenta niezbędnego do awansu na kolejny poziom, na przykład:
Mini |
Poziom |
Do następnego poz. |
---|---|---|
Gnol wojownik |
3 |
20% |
Gryfi jeździec |
2 |
20% |
Pilotka O.P.O.R. |
2 |
20% |
Po grze dokonujemy szybkich obliczeń. Na przykład Gnol wojownik otrzymuje +3 PD, procentowo to +3 / 10, dajmy Gnolowi wojownikowi 30% następnego poziomu, co skutkuje:
Mini |
Poziom |
Do następnego poz. |
---|---|---|
Gnol wojownik |
3 |
50% |
Gryfi jeździec |
2 |
20% |
Pilotka O.P.O.R. |
2 |
20% |
Zapewniłoby nam to elastyczność po zmianie krzywej poziomu mini, ale nie poskutkowałoby zmianą poziomu dla mini. Umożliwiłoby nam to również łatwiejszą wizualizację w interfejsie użytkownika w grze – wystarczy napełnić 30% paska, bez konieczności wykonywania obliczeń. Taki wzorzec poskutkowałby bardzo małymi wartościami procentowymi na wyższych poziomach. Gdy mini otrzymuje 10 PD za grę, a wymaga 100 000 PD do awansu, wartość byłaby przedstawiana jako 0,01%. Procesory poradzą sobie z liczbami zmiennoprzecinkowymi, ale precyzja i dokładność wiąże się z błędami, jeśli nie weźmiemy pod uwagę natury obliczeń zmiennoprzecinkowych.
Do następnego razu…
Rozpatrywaliśmy wiele możliwości w zakresie technologii baz danych, narzędzi oraz przechowywania i obliczeń. Jak ze wszystkim w fazie projektowania, może to jeszcze ulec zmianie, jeśli znajdziemy lepsze rozwiązanie, ale obecny mechanizm zdaje się być dobrym punktem wyjściowym.
Dziękujemy za lekturę!
~ Andy Lim
Oficjalnie zaznaczam, że to nie ja umieszczam zabawkowe oczka w biurze. W mój pierwszy dzień w zespole moje nowiutkie monitory zostały nimi obklejone. Obserwują mnie cały dzień... cały... dzień.