Gdy coś w kodzie Verse nie działa zgodnie z oczekiwaniami, czasami trudno jest ustalić, co poszło nie tak. Przykładowe problemy, jakie możesz napotkać:
- Błędy w czasie wykonywania programu.
- Kod wykonywany w niewłaściwej kolejności.
- Procesy trwają dłużej niż powinny.
Każdy z nich może spowodować nieoczekiwane zachowanie kodu, a w konsekwencji problemy w trakcie rozgrywki. Diagnozowanie problemów w kodzie nazywa się debugowaniem. Istnieje kilka różnych rozwiązań, które można zastosować, aby naprawić i zoptymalizować swój kod.
Błędy w czasie wykonywania programu Verse
Kod Verse jest analizowany zarówno podczas pisania go na serwerze języka, jak i podczas kompilowania z poziomu edytora lub Visual Studio Code. Jednak ta analiza semantyczna sama w sobie nie jest w stanie wychwycić wszystkich możliwych problemów, jakie możesz napotkać. Gdy kod jest wykonywany w czasie wykonywania programu, może spowodować wyzwolenie błędów w czasie wykonywania programu. Spowodują one zaprzestanie dalszego wykonywania kodu Verse, przez co rozegranie przygody może być niemożliwe.
Przypuśćmy na przykład, że masz kod Verse, który wykonuje następujące czynności:
# Zawiera specyfikator suspends, więc może być wywoływany w wyrażeniu loop.
SuspendsFunction()<suspends>:void={}
# Wywołuje funkcję SuspendFunction w nieskończoność, bez przerywania czy zwracania,
# powodując błąd w czasie wykonywania programu spowodowany nieskończoną pętlą.
CausesInfiniteLoop()<suspends>:void=
loop:
SuspendsFunction()
Funkcja CausesInfiniteLoop()
nie spowodowałaby żadnych błędów w kompilatorze Verse, a program zostałby pomyślnie skompilowany. Jeśli jednak wywołasz CausesInfiniteLoop()
w czasie wykonywania programu, funkcja uruchomi pętlę nieskończoną, a tym samym wyzwoli błąd w czasie wykonywania programu.
Aby zapoznać się z błędami w czasie wykonywania programu, które wystąpiły w twojej grze, przejdź do serwisu Content Service Portal (Portal obsługi zawartości). Znajdziesz tam listę wszystkich swoich projektów, zarówno opublikowanych, jak i nieopublikowanych. Przy każdym projekcie masz dostęp do karty Verse, która zawiera listę kategorii błędów w czasie wykonywania programu, jakie wystąpiły w projekcie. Możesz też sprawdzić stos wywołań Verse, w którym dany błąd został zgłoszony, co pozwoli uzyskać więcej szczegółów na temat tego, co mogło pójść nie tak. Raporty o błędach są przechowywane przez okres do 30 dni.

Pamiętaj, że jest to nowa funkcja na wczesnym etapie prac rozwojowych, a sposób jej działania może ulec zmianie w przyszłych wersjach UEFN i Verse.
Profilowanie wolno działającego kodu
Jeśli kod działa wolniej niż oczekiwano, możesz go przetestować za pomocą wyrażenia profile. Wyrażenie profile informuje, jak długo trwa uruchomienie określonego fragmentu kodu, i może pomóc w zidentyfikowaniu wolno działających bloków kodu i ich optymalizacji. Wyobraźmy sobie na przykład, że chcemy sprawdzić, czy tablica zawiera konkretną liczbę, i zwrócić indeks w miejscu jej występowania. Można to zrobić, wykonując iterację po tablicy i sprawdzając, czy poszczególne liczby są zgodne z poszukiwaną.
# Tablica liczb testowych.
TestNumbers:[]int = array{1,2,3,4,5}
# Działa po uruchomieniu urządzenia w aktywnej grze
OnBegin<override>()<suspends>:void=
# Sprawdź, czy liczba występuje w tablicy TestNumbers poprzez iterację
# przez każdy element i sprawdzanie, czy jest zgodny.
for:
Index -> Number:TestNumbers
Number = 4
do:
Print("Znaleziono liczbę pod indeksem {Index}!")
Jest to jednak mało wydajny kod, ponieważ wymaga sprawdzania każdej liczby w tablicy pod kątem zgodności. Prowadzi to do mało wydajnej złożoności czasowej, ponieważ sprawdzanie listy będzie kontynuowane, nawet jeśli poszukiwany element zostanie znaleziony. Zamiast tego można użyć funkcji Find[]
, aby sprawdzić, czy tablica zawiera szukaną liczbę, a następnie ją zwrócić. Z uwagi na to, że funkcja Find[]
zwraca wynik bezpośrednio po znalezieniu elementu, zostanie wykonana tym szybciej, im wcześniej na liście będzie znajdował się poszukiwany element. Jeśli do przetestowania obu fragmentów kodu użyjesz wyrażenia profile
, zobaczysz, że w tym przypadku kod używający funkcji Find[]
powoduje skrócenie czasu wykonania.
# Działa po uruchomieniu urządzenia w aktywnej grze
OnBegin<override>()<suspends>:void=
# Sprawdź, czy liczba występuje w tablicy TestNumbers poprzez iterację
# przez każdy element i sprawdzanie, czy jest zgodny.
profile("Znajdowanie liczby poprzez sprawdzenie każdego elementu tablicy"):
for:
Index -> Number:TestNumbers
Number = 4
do:
Print("Znaleziono liczbę pod indeksem {Index}!")
# Korzystając z funkcji Find[], sprawdź, czy liczba występuje.
profile("Znajdowanie liczby za pomocą funkcji Find[]"):
if:
FoundIndex := TestNumbers.Find[4]
then:
Print("Znaleziono liczbę pod indeksem {FoundIndex}!")
else:
Print("Nie znaleziono liczby!")
Te niewielkie różnice w czasie wykonywania stają się większe, im więcej elementów musi obejmować iteracja. Każde wyrażenie wykonywane podczas iteracji po dużej liście zwiększa złożoność czasową, zwłaszcza gdy tablice rozrastają się do setek, a nawet tysięcy elementów. Skalując przygody pod kątem coraz większej liczby graczy, użyj wyrażenia profile
, aby odszukać kluczowe obszary powodujące spowolnienie i się z nimi uporać.
Rejestratory i dane wyjściowe rejestrowania
Domyślnie po wywołaniu funkcji Print()
w kodzie Verse w celu wyświetlenia komunikatu, komunikat ten jest zapisywany w specjalnym dzienniku Print
. Wyświetlane komunikaty pojawiają się na ekranie w trakcie gry, w dzienniku gry oraz w dzienniku wyjściowym w UEFN.
Gdy wyświetlasz komunikat przy użyciu funkcji Print(), komunikat jest zapisywany w dzienniku wyjściowym, na karcie Dziennik w grze oraz wyświetlany na ekranie w trakcie gry.
Jednak w wielu przypadkach możesz nie chcieć, aby komunikaty pojawiały się na ekranie w trakcie gry. Komunikaty możesz wykorzystywać do śledzenia momentów, w których coś się dzieje, na przykład następuje uruchomienie zdarzenia lub upływa określona ilość czasu, albo do sygnalizowania problemów z kodem. Wiele komunikatów w trakcie rozgrywki może rozpraszać uwagę, zwłaszcza jeśli nie przekazują graczowi żadnych istotnych informacji.
Aby rozwiązać ten problem, można użyć rejestratora. Rejestrator to specjalna klasa, która umożliwia wyświetlanie komunikatów bezpośrednio w kartach Dziennik wyjściowy i Dziennik, bez wyświetlania ich na ekranie.
Rejestratory
Aby utworzyć rejestrator, należy najpierw utworzyć kanał dziennika. Każdy rejestrator zapisuje komunikaty w dzienniku wyjściowym, jednak ustalenie, który komunikat pochodzi z którego rejestratora, może być trudne. Kanały dziennika dodają nazwę kanału dziennika na początku komunikatu, aby ułatwić sprawdzenie, który rejestrator wysłał komunikat. Kanały dziennika są deklarowane w modułach, podczas gdy rejestratory są deklarowane wewnątrz klas lub funkcji. Poniżej przedstawiono przykład deklarowania kanału dziennika w module, a następnie przykład deklarowania i wywoływania rejestratora wewnątrz urządzenia Verse.
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/Diagnostics }
# Kanał dziennika dla klasy debugging_tester.
# Kanały dziennika zadeklarowane w module mogą być używane przez dowolną klasę.
debugging_tester_log := class(log_channel){}
# Urządzenie trybu kreatywnego utworzone w Verse, które można umieścić w poziomie
debugging_tester := class(creative_device):
# Rejestrator lokalny dla klasy debugging_tester.
Logger:log = log{Channel := debugging_tester_log}
# Działa po uruchomieniu urządzenia w aktywnej grze
OnBegin<override>()<suspends>:void=
Print("Z tej strony kanał instrukcji print!")
Logger.Print("A teraz tester debugowania!")
Gdy wyświetlasz komunikat przy użyciu funkcji rejestratora Print(), komunikat jest zapisywany w dzienniku wyjściowym i na karcie Dziennik w grze.
Poziomy dzienników
Oprócz kanałów, można również zdefiniować domyślny poziom dziennika, w którym rejestrator będzie zapisywał dane. Dostępnych jest pięć poziomów, a każdy z nich ma odrębne właściwości:
Poziom dziennika | Miejsce zapisu komunikatu | Właściwości specjalne |
---|---|---|
Debugowanie | Dziennik w grze | Nie dotyczy |
Szczegółowy | Dziennik w grze | Nie dotyczy |
Normalny | Dziennik w grze, Dziennik wyjściowy | Nie dotyczy |
Ostrzegawczy | Dziennik w grze, Dziennik wyjściowy | Tekst jest żółty |
Błędy | Dziennik w grze, Dziennik wyjściowy | Tekst jest czerwony |
Gdy tworzysz rejestrator, domyślnie przyjmuje on poziom dziennika Normal
. Poziom rejestratora można zmienić podczas tworzenia rejestratora lub określić go podczas wywoływania funkcji Print()
.
# Rejestrator lokalny dla klasy debugging_tester. Domyślnie wyświetla się ona
# do log_level.Normal.
Logger:log = log{Channel := debugging_tester_log}
# Rejestrator z log_level.Debug jako domyślnym kanałem dziennika.
DebugLogger:log = log{Channel := debugging_tester_log, DefaultLevel := log_level.Debug}
# Działa po uruchomieniu urządzenia w aktywnej grze
OnBegin<override>()<suspends>:void=
# Rejestrator domyślnie wysyła komunikaty do kanału dziennika poziomu normalnego, natomiast DebugLogger
# domyślnie wysyła komunikaty do kanału dziennika debugowania. Każdy rejestrator może zapisywać dane na dowolnym poziomie poprzez
# określanie argumentu ?Level podczas wywoływania funkcji Print()
Logger.Print("Ten komunikat jest wysyłany na kanale dziennika poziomu normalnego!")
DebugLogger.Print("A ten komunikat jest wysyłany na kanale dziennika poziomu debugowania!")
Logger.Print("Ten komunikat również może być wysłany do kanału debugowania!", ?Level := log_level.Debug)
W powyższym przykładzie Logger
używa domyślnie kanału dziennika Normal
, podczas gdy DebugLogger
używa domyślnie kanału dziennika Debug
. Każdy rejestrator może zapisywać dane w dzienniku dowolnego poziomu poprzez zdefiniowanie log_level
podczas wywoływania funkcji Print()
.
Rezultaty zastosowania rejestratora do zapisywania danych na różnych poziomach dziennika. Zwróć uwagę, że log_level.Debug i log_level.Verbose nie zapisują danych w dzienniku gry, a jedynie w dzienniku wyjściowym UEFN.
Wyświetlanie stosu wywołań
Stos wywołań śledzi listę wywołań funkcji, które doprowadziły do bieżącego zakresu. Jest to rodzaj skumulowanego zestawu instrukcji, których używa kod, aby wiedzieć, dokąd ma wrócić, gdy zakończy się wykonywanie bieżącej procedury. Stos wywołań z dowolnego rejestratora można wyświetlić za pomocą funkcji PrintCallStack()
. Weźmy na przykład następujący kod:
# Rejestrator lokalny dla klasy debugging_tester. Domyślnie ten rejestrator zapisuje dane go
# log_level.Normal.
Logger:log = log{Channel := debugging_tester_log}
# Działa po uruchomieniu urządzenia w aktywnej grze
OnBegin<override>()<suspends>:void=
# Przejdź do pierwszej funkcji i po kilku poziomach wyświetl stos wywołań.
LevelOne()
# Wywołuje LevelTwo(), aby przejść o jeden poziom niżej.
LevelOne():void=
LevelTwo()
# Wywołuje LevelThree(), aby przejść o jeden poziom niżej.
LevelTwo():void=
LevelThree()
# Wyświetl stos wywołań, który przedstawia sekwencję
# wywołań funkcji, które doprowadziły do tego punktu.
LevelThree():void=
Logger.PrintCallStack()
Powyższy kod w funkcji OnBegin()
powyżej wywołuje LevelOne()
, aby przejść do pierwszej funkcji. Następnie funkcja LevelOne()
wywołuje funkcję LevelTwo()
, która wywołuje funkcję LevelThree()
, która wywołuje funkcję Logger.PrintCallStack()
w celu wyświetlenia bieżącego stosu wywołań. Najnowsze wywołanie będzie znajdować się na szczycie stosu, dlatego jako pierwsza wyświetli się funkcja LevelThree()
. Następnie będą to funkcje LevelTwo()
, LevelOne()
i OnBegin()
.
Gdy w kodzie coś jest nie tak, wyświetlenie stosu wywołań pozwala dowiedzieć się, jakie wywołania doprowadziły do danego punktu. Ułatwia to przeglądanie struktury kodu w trakcie jego wykonywania i umożliwia wyizolowanie poszczególnych śladów w stosie w przypadku projektów o dużej gęstości kodu.
Wizualizacja danych gry za pomocą szkicu debugowania
Innym sposobem debugowania różnych funkcji na potrzeby tworzonych przygód jest użycie interfejsu API szkicu debugowania. Pozwala on tworzyć kształty do debugowania w celu wizualizacji danych gry. Oto kilka przykładów:
- Linia wzroku strażnika.
- Odległość, na jaką przenośnik rekwizytów przesunie obiekt.
- Zasięg wytłumiania odtwarzacza dźwięku.
Możesz użyć tych kształtów do debugowania, aby precyzyjnie dostosować swoją przygodę bez uwidaczniania tych danych w opublikowanych przygodach. Aby dowiedzieć się więcej, patrz Szkic debugowania w Verse.
Optymalizacja i synchronizacja przy użyciu współbieżności
Współbieżność jest istotą języka programowania Verse i potężnym narzędziem pozwalającym uatrakcyjnić tworzone przygody. Dzięki współbieżności jedno urządzenie Verse może wykonywać wiele operacji jednocześnie. Pozwala to pisać bardziej elastyczny, zwięzły kod i ograniczyć liczbę urządzeń używanych w poziomie. Współbieżność to świetne narzędzie do optymalizacji, a znalezienie sposobów na wykorzystanie kodu asynchronicznego do obsługi wielu zadań jednocześnie to świetny rozwiązanie pozwalające przyspieszyć wykonywanie programów i uporać się z problemami związanymi z synchronizacją czasową.
Tworzenie kontekstów asynchronicznych za pomocą wyrażenia spawn
Wyrażenie spawn
rozpoczyna wyrażenie asynchroniczne z dowolnego kontekstu, umożliwiając jednocześnie natychmiastowe wykonanie następujących po nim wyrażeń. Dzięki temu można uruchamiać wiele zadań w tym samym czasie na tym samym urządzeniu, bez konieczności tworzenia nowych plików Verse dla każdego z nich. Rozważmy na przykład scenariusz, w którym kod co sekundę monitoruje zdrowie każdego gracza. Jeśli zdrowie gracza spadnie poniżej określonej wartości, gracz zostanie w niewielkim stopniu uleczony. Potem chcesz wykonać kod odpowiedzialny za realizację innego zadania. Urządzenie, które implementuje ten kod, może wyglądać mniej więcej tak:
# Urządzenie trybu kreatywnego utworzone w Verse, które można umieścić w poziomie
healing_device := class(creative_device):
# Działa po uruchomieniu urządzenia w aktywnej grze
OnBegin<override>()<suspends>:void=
AllPlayers:[]agent = GetPlayspace().GetPlayers()
# Co sekundę sprawdź każdego gracza. Jeśli gracz ma mniej niż połowę zdrowia,
# leczy go o niewielką wartość.
loop:
for:
Player:Players
Character := Player.GetFortCharacter[]
PlayerHP := Character.GetHealth()
PlayerHP <= HPThreshold
do:
Character.SetHealth(PlayerHP + SmallHeal)
Sleep(1.0)
Print("To jest reszta kodu!")
Jednak pętla ta jest wykonywana w nieskończoność i nigdy nie dochodzi do jej przerwania, dlatego następujący po niej kod nigdy nie zostanie wykonany. Jest to projekt ograniczający, ponieważ urządzenie utyka na wykonywaniu wyrażenia loop. Aby umożliwić urządzeniu wykonywanie wielu czynności jednocześnie i współbieżne wykonywanie kodu, możesz przenieść kod loop
do funkcji asynchronicznej i zespawnować go podczas wykonywania funkcji OnBegin()
.
# Działa po uruchomieniu urządzenia w aktywnej grze
OnBegin<override>()<suspends>:void=
# Użyj wyrażenia spawn, aby uruchomić HealMonitor() w sposób asynchroniczny.
spawn{HealMonitor()}
# Następujący po nim kod zostanie niezwłocznie wykonany.
Print("Ten kod jest wykonywany podczas wykonywania wyrażenia spawn")
HealMonitor(Players:[]agent)<suspends>:void=
# Co sekundę sprawdź każdego gracza. Jeśli gracz ma mniej niż połowę zdrowia,
# leczy go o niewielką wartość.
loop:
for:
Player:Players
Character := Player.GetFortCharacter[]
PlayerHP := Character.GetHealth()
PlayerHP <= HPThreshold
do:
Character.SetHealth(PlayerHP + SmallHeal)
Sleep(1.0)
Jest to usprawnienie, ponieważ urządzenie może teraz uruchamiać inny kod w trakcie wykonywania funkcji HealMonitor()
. Jednak funkcja wciąż musi wykonać pętlę po poszczególnych graczach, co może powodować problemy z synchronizacją czasu, gdy w przygodzie uczestniczy wielu graczy. Załóżmy, że chcesz przyznać każdemu graczowi punkty w oparciu o jego PZ albo sprawdzić, czy gracz posiada dany przedmiot. Dodanie dodatkowej logiki dla każdego gracza w wyrażeniu for
dodatkowo zwiększa złożoność czasową tej funkcji i przy wystarczającej liczbie graczy jeden gracz może nie zostać uleczony na czas, jeśli odniesie obrażenia wynikające z problemu z synchronizacją czasową.
Zamiast wykonywania pętli po poszczególnych graczach i sprawdzania każdego z nich z osobna, można dodatkowo zoptymalizować ten kod, spawnując instancję funkcji dla każdego gracza. To oznacza, że jedna funkcja może monitorować pojedynczego gracza, dzięki czemu kod nie musi sprawdzać każdego gracza z osobna, zanim pętka powróci do tego, który wymaga leczenia. Wykorzystanie wyrażeń współbieżności, takich jak spawn
może sprawić, że kod będzie bardziej wydajny i elastyczny, a pozostała część bazy kodu zostanie uwolniona do wykonywania innych zadań.
# Działa po uruchomieniu urządzenia w aktywnej grze
OnBegin<override>()<suspends>:void=
AllPlayers := GetPlayspace().GetPlayers()
# Zespawnuj instancję funkcji HealMonitor() dla każdego gracza.
for:
Player:AllPlayers
do:
# Użyj wyrażenia spawn, aby wykonać funkcję HealMonitorPerPlayer() w sposób asynchroniczny.
spawn{HealMonitorPerPlayer(Player)}
# Następujący po nim kod zostanie niezwłocznie wykonany.
Print("Ten kod jest wykonywany podczas wykonywania wyrażenia spawn")
HealMonitorPerPlayer(Player:agent)<suspends>:void=
if:
Character := Player.GetFortCharacter[]
then:
# Co sekundę sprawdzaj monitorowanego gracza. Jeśli gracz ma mniej niż połowę zdrowia,
# leczy go o niewielką wartość.
loop:
PlayerHP := Character.GetHealth()
if:
PlayerHP <= HPThreshold
then:
Character.SetHealth(PlayerHP + SmallHeal)
Sleep(1.0)
Użycie wyrażenia spawn
w wyrażeniu loop
może spowodować niepożądane zachowanie, jeśli wyrażenie zostanie przygotowane błędnie. Przykładowo funkcja HealMonitorPerPlayer()
nigdy się nie kończy, dlatego kod ten będzie spawnował nieskończoną liczbę funkcji asynchronicznych, dopóki nie wystąpi błąd w czasie wykonywania programu.
# Zespawnuj instancję funkcji HealMonitor() dla każdego gracza z nieskończoną pętlą.
# Spowoduje to błąd w czasie wykonywania programu, ponieważ liczba funkcji asynchronicznych będzie wzrastać w nieskończoność.
loop:
for:
Player:AllPlayers
do:
spawn{HealMonitorPerPlayer(Player)}
Sleep(0.0)
Kontrolowanie synchronizacji czasowej za pomocą zdarzeń
Prawidłowe zsynchronizowanie wszystkich części kodu bywa trudne, zwłaszcza w dużych przygodach wieloosobowych, w których równocześnie wykonywanych jest wiele skryptów. Różne części kodu mogą być zależne od odmiennych funkcji lub skryptów wykonywanych w ustalonej kolejności, a to w przypadku braku ścisłej kontroli może powodować problemy z synchronizacją między nimi. Przyjrzyjmy się na przykład poniższej funkcji, która odlicza przez pewien czas, a następnie przyznaje przekazanemu do niej graczowi punkty, jeśli liczba jego PZ jest większa niż wartość progowa.
CountdownScore(Player:agent)<suspends>:void=
# Odczekaj chwilę, a następnie przyznaj punkty każdemu graczowi, którego liczba PZ jest wyższa od wartości progowej.
Sleep(CountdownTime)
if:
Character := Player.GetFortCharacter[]
PlayerHP := Character.GetHealth()
PlayerHP >= HPThreshold
then:
ScoreManager.Activate(Player)
Funkcja ta ma modyfikator <suspends>
, więc możesz wykonywać jej instancję w sposób asynchroniczny dla każdego gracza za pomocą funkcji spawn()
. Musisz jednak zagwarantować, że każdy inny kod zależny od tej funkcji był uruchamiany zawsze po jej zakończeniu. Załóżmy, że chcesz wyświetlić każdego gracza, który zdobył punkt po zakończeniu funkcji CountdownScore()
? Możesz to zrobić w funkcji OnBegin()
, wywołując Sleep()
, aby odczekać tyle samo czasu, ile zajmuje wykonanie CountdownScore()
, ale może to spowodować problemy z synchronizacją czasową, gdy gra jest uruchomiona i wprowadza nową zmienną, którą trzeba aktualizować za każdym razem, gdy wprowadzasz zmiany w swoim kodzie. Zamiast tego możesz utworzyć zdarzenia niestandardowe i wywołać na nich funkcję Await()
, aby ściśle kontrolować kolejność wykonywania zdarzeń w kodzie.
# Zdarzenie niestandardowe, które ma sygnalizować, gdy odliczanie dobiegnie końca.
CountdownCompleteEvent:event() = event(){}
# Działa po uruchomieniu urządzenia w aktywnej grze
OnBegin<override>()<suspends>:void=
AllPlayers := GetPlayspace().GetPlayers()
# Zespawnuj funkcję CountdownScore dla każdego gracza
for:
Player:AllPlayers
do:
spawn{CountdownScore(Player)}
# Oczekuj na zasygnalizowanie zdarzenia CountdownCompletedEvent.
CountdownCompleteEvent.Await()
# Jeśli gracz ma jakiekolwiek punkty, wyświetl go w dzienniku.
for:
Player:AllPlayers
CurrentScore := ScoreManager.GetCurrentScore(Player)
CurrentScore > 0
do:
Print("Ten gracz otrzymał punkty!")
CountdownScore(Player:agent)<suspends>:void=
# Odczekaj chwilę, a następnie przyznaj punkty każdemu graczowi, którego liczba PZ jest wyższa od wartości progowej.
Sleep(CountdownTime)
if:
Character := Player.GetFortCharacter[]
PlayerHP := Character.GetHealth()
PlayerHP >= HPThreshold
then:
ScoreManager.Activate(Player)
# Zasygnalizuj zdarzenie, aby umożliwić kontynuację dowolnego kodu oczekującego na ten sygnał.
CountdownCompleteEvent.Signal()
Kod oczekuje teraz na zasygnalizowanie zdarzenia CountdownCompletedEvent()
, więc wynik każdego gracza zostanie sprawdzony dopiero po zakończeniu wykonywania CountdownScore()
. Wiele urządzeń ma wbudowane zdarzenia, na których można wywołać funkcję Await()
, aby kontrolować synchronizację czasową kodu. Wykorzystując je we własnych zdarzeniach niestandardowych, możesz tworzyć złożone pętle gry z kilkoma ruchomymi częściami. Na przykład Szablon pakietu startowego Verse wykorzystuje kilka niestandardowych zdarzeń do kontrolowania ruchu postaci, aktualizowania UI i zarządzania ogólną pętlą gry między planszami.
Obsługa wielu wyrażeń za pomocą wyrażeń sync, race i rush
Wyrażenia sync, race i rush umożliwiają uruchamianie wielu wyrażeń asynchronicznych równocześnie, przy jednoczesnym wykonywaniu różnych funkcji po zakończeniu wykonywania tych wyrażeń. Wykorzystując każdą z tych możliwości, można ściśle kontrolować czas życia każdego z wyrażeń asynchronicznych, czego efektem jest bardziej dynamiczny kod, który jest w stanie obsłużyć wiele różnych sytuacji.
Weźmy na przykład wyrażenie rush
. To wyrażenie uruchamia współbieżnie wiele wyrażeń asynchronicznych, ale zwraca tylko wartość wyrażenia, które kończy się jako pierwsze. Przypuśćmy, że masz minigrę, w której drużyny muszą wykonać jakieś zadanie, a drużyna, która ukończy zadanie jako pierwsza, otrzymuje wzmocnienie pozwalające przeszkadzać innym graczom, gdy starają się również skończyć grę. Można napisać skomplikowaną logikę synchronizacji czasu, aby śledzić ukończenie zadania przez każdą drużynę, lub użyć wyrażenia rush
. Wyrażenie zwraca wartość pierwszego wyrażenia asynchronicznego do zakończenia, dlatego zwróci zwycięską drużynę, jednocześnie zezwalając na wykonywanie kodu, który obsługuje inne drużyny.
WinningTeam := rush:
# Wszystkie trzy funkcje asynchroniczne uruchamiane są w tym samym czasie.
RushToFinish(TeamOne)
RushToFinish(TeamTwo)
RushToFinish(TeamThree)
# Następne wyrażenie jest wywoływane natychmiast po zakończeniu dowolnej z funkcji asynchronicznych.
GrantPowerup(WinnerTeam)
Wyrażenie race
podlega tym samym regułom, z tą różnicą, że po zakończeniu wyrażenia asynchronicznego pozostałe wyrażenia zostają anulowane. Pozwala to na ścisłą kontrolę czasu życia wielu wyrażeń asynchronicznych jednocześnie. Można nawet połączyć to wyrażenie z wyrażeniem sleep()
, aby ograniczyć ilość czasu, przez jaki wyrażenie ma być wykonywane. Rozważmy przykład użycia wyrażenia rush
, w którym tym razem chcesz, aby minigra zakończyła się natychmiast po zwycięstwie drużyny. Chcesz również dodać licznik czasu, aby minigra nie trwała w nieskończoność. Wyrażenie race
umożliwia wykonywanie obu tych czynności bez konieczności używania zdarzeń lub innych narzędzi współbieżności, aby wiedzieć, kiedy anulować wyrażenia, które przegrywają w wyścigu obsługiwanym przez wyrażenie race.
WinningTeam := race:
# Wszystkie cztery funkcje asynchroniczne uruchamiają się w tym samym czasie.
RaceToFinish(TeamOne)
RaceToFinish(TeamTwo)
RaceToFinish(TeamThree)
Sleep(TimeLimit)
# Następne wyrażenie jest wywoływane natychmiast po zakończeniu dowolnej z funkcji asynchronicznych. Wszelkie inne funkcje asynchroniczne zostają anulowane.
GrantPowerup(WinnerTeam)
Wreszcie wyrażenie sync
umożliwia oczekiwanie na zakończenie wykonywania wielu wyrażeń, gwarantując, że każde z nich zostanie ukończone przed kontynuacją. Wyrażenie sync
zwraca krotkę zawierającą wyniki każdego z wyrażeń asynchronicznych, dlatego możesz zakończyć działanie wszystkich swoich wyrażeń i ocenić dane uzyskane z każdego z nich z osobna. Wracając do przykładu minigry. Załóżmy, że chcesz przyznać wzmocnienia każdej drużynie zgodnie z wynikami, jakie uzyskała w minigrze. Do tego właśnie posłuży wyrażenie sync
.
TeamResults := sync:
# Wszystkie trzy funkcje asynchroniczne uruchamiane są w tym samym czasie.
WaitForFinish(TeamOne)
WaitForFinish(TeamTwo)
WaitForFinish(TeamThree)
# Następne wyrażenie jest wywoływane tylko wtedy, gdy wszystkie wyrażenia asynchroniczne zostaną ukończone.
GrantPowerups(TeamResults)
Jeśli chcesz uruchomić wyrażenie asynchroniczne na wielu elementach tablicy, możesz użyć przydatnej funkcji ArraySync()
, aby zagwarantować synchronizację.
Każde z tych wyrażeń współbieżności jest samo w sobie potężnym narzędziem, a poznając sposób ich łączenia i wykorzystywania, możesz napisać kod, który poradzi sobie w każdej sytuacji. Przyjrzyj się przykładowi z szablonu Wyścig szosowy z persystencją Verse, który łączy wiele wyrażeń współbieżności, aby nie tylko odtworzyć każdemu graczowi wstęp przed wyścigiem, ale także anulować odtwarzanie, gdy gracz opuści grę w trakcie wstępu. Ten przykład pokazuje, jak można wykorzystywać współbieżność na wiele sposobów i tworzyć elastyczny kod, który dynamicznie reaguje na różne zdarzenia.
# Poczekaj na rozpoczęcie intro gracza i wyświetl jego informacje.
# Anuluj oczekiwanie, jeśli gracz odejdzie z gry.
WaitForPlayerIntro(Player:agent, StartOrder:int)<suspends>:void=
var IntroCounter:int = 0
race:
# Poczekaj, aż ten gracz ukończy wyścig, a następnie zarejestruj ukończenie.
loop:
sync:
block:
StartPlayerIntroEvent.TriggeredEvent.Await()
if (IntroCounter = StartOrder):
PlayerLeaderboard.UpdatePopupUI(Player, PopupDialog)
EndPlayerIntroEvent.TriggeredEvent.Await()
if (IntroCounter = StartOrder):
break
set IntroCounter += 1
# Poczekaj, aż ten gracz opuści grę.
loop:
LeavingPlayer := GetPlayspace().PlayerRemovedEvent().Await()
if:
LeavingPlayer = Player
then:
break