Pobieranie danych JSON z sieci - wyświetlanie aktualności w grze
Wtorek, 08 Listopada 2022, 23:43
Czas czytania 7 minut, 15 sekund
Zgodne z GM:
Przykład pobierania informacji z sieci - w naszym przypadku posłuży do wyświetlania aktualności, ale może to być też lista wyników, czy ceny przedmiotów.
Od wersji 2.3 GameMaker pozwala na korzystanie ze struktur, które znacznie ułatwiają przetwarzanie danych - zamiast skomplikowanej kombinacji ds_map i ds_list, możemy znacznie łatwiej operować na danych.
Wyobraźmy sobie więc, że jakieś dane chcemy umieszczać na własnym serwerze, aby gra mogła dynamicznie je pobrać i je wyświetlić, lub nawet dostosować do nich gameplay.
Nie jest to wcale trudne i wymaga dosłownie kilkunastu minut roboty.
Tworzenie zapytania - Create
Stwórzmy nowy projekt, a w nim jeden obiekt. Będzie się on składał z zaledwie trzech zmiennych, oraz definicji enum którą dla ułatwienia tutaj wrzuciłem - warto jednak pamiętać, że enumy są globalne i podmieniane są w trakcie kompilacji na kolejne liczby rzeczywiste, więc ten fragment kodu tak naprawdę wyparuje.
Statusy pobierania danych będą 4 - początkowy, czyli wczytywanie, sukces, oraz timeout i błąd. Poza tym, potrzebujemy zmiennej do które zapiszemy pobrane przez nas dane (content), obecnego statusu (state) i identyfikatora zapytania (request), który rozpocznie zapytanie pod wskazany przez nas adres.
kod // statusy - otrzymają kolejne wartości 0,1,2,...
enum states {
loading,
succes,
timeout,
error
}
// zmienne
content = ""; // treśc którą wyświetlimy
state = states.loading; // ustawmy domyślny status
request = http_get("gmclan.org/.../example_news.json"); // rozpocznij zapytanie http
Ot i 1/3 projektu za nami!
Wyświetlanie wyniku - Draw
Jak już wspomniałem, mamy 4 statusy - zrobimy teraz ich wyświetlanie. Wiemy już, że każdy status poza success oznacza, że albo zapytanie trwa, albo coś nie wyszło. Obsłużmy więc pozostałe 3 osobno i wyświetlajmy krótki komunikat. Oczywiście, w samej grze być może chcielibyśmy wyświetlać jakiś animowany sprite robiący za pasek wczytywania. Nie zapomnijmy też stworzyć fonta, który będzie zawierał polskie znaki diakrytyczne - ja swój nazwałem
Wskazówka:
Domyślny font w GameMaker nie wyświetla polskich znaków. Musimy więc dodać nowy font, kliknąć "Add", a następnie wpisać
kod // ustawmy font, zawierający polskie znaki
draw_set_font(fnt_default);
if (state != states.succes) {
// jeśli wczytujemy lub jest błąd
var msg = "Wczytywanie...";
switch (state) {
case states.error:
msg = "Błąd pobierania.";
break;
case states.timeout:
msg = "Nie udało się wczytać adresu.";
break;
}
draw_text(10, 10, msg);
} else {
// wypiszmy newsy
draw_text(10, 10, content);
}
Obsługa zapytania - Async: HTTP
Teraz musimy obsłużyć moment, w którym GM wczytał (lub nie) dane z naszego adresu. Wykorzystamy event asynchroniczny HTTP.
Wskazówka:
Należy pamiętać, że jeśli więcej niż jeden obiekt ma wydarzenie Async HTTP, to w momencie gdy nasze zapytanie dobiegnie końca, zdarzenie to odpali się we wszystkich instancjach wszystkich obiektów bez względu czy to one rozpoczęły dane zapytanie. Właśnie dlatego przypisaliśmy je do zmiennej
Wskazówka:
Więcej o evencie Async HTTP znajdziecie w dokumetnacji: manual.yoyogames.com/.../HTTP.htm .
Dla nas najważniejsza informacją jest, że w tym evencie dostajemy specjalną, wygenerowaną tymczasowo przez GMa ds_mapę o nazwie
Interesują nas jej trzy klucze:
- id - które zawiera identyfikator requestu który odpalił ten event. W naszej grze request jest jeden, ale "na przyszłość" uczymy się pisać bezpieczniejszy kod
- status - jeśli jest on mniejszy niż 0, to wystąpił błąd
- result - tekst który zwróciła witryna, bez nagłówków
W pierwszej kolejności zajmiemy się osbługą błędów (status < 0) i za pomocą exit zaprzestaniemy dalszego wykonywania kodu.
W drugim kroku spróbujemy sparsować JSONa którego otrzymaliśmy i wyświetlić dane.
Co jednak, gdy natrafimy na błąd serwera (np. przestanie on działać, dostaniemy 404, zmieni się struktura JSONa)? Należałoby po kolei sprawdzać, czy json się sparsował, czy zawiera wymagane przez nas dane itd.
GameMaker od wersji 2.3 (oraz w wersjach 2021+) oferuje jednak mechanizm dzięki któremu możemy sobie pozwolić być leniwymi i totalnie olać to sprawdzanie. Dzięki try-catch będziemy przetwarzać dane, a jeśli pojawi się jakiś błąd, uznajemy, że JSON nie jest poprawny i ustawiamy "state" na "error".
Wskazówka:
Korzystając z try-catch, nie dostaniemy okienka z błędem i gra będzie działać dalej, ale z drugiej strony przerwie wykonywanie kodu wewnątrz try{} przy pierwszym błędzie. Jeśli wiemy, że jakaś zmienna może, ale nie musi istnieć, można skorzystać z funkcji is_undefined() lub z
Nasz przykładowy JSON znajdziecie tutaj:
gmclan.org/upload/gm/examples/example_news.json
Zawiera on pod własnością "news" tablicę, która składa się z własności title, content, date. Wiemy więc, że w danych jakie otrzymamy po sparsowaniu JSONa, będzie własność
Wskazówka:
Wartości w zmiennej date wygenerowałem wcześniej za pomocą
Pętle for nie dopuszczają przecinków w swojej składni, ale wykorzystujemy fakt, że pozwala na taką składnię konstrukcja var.
W kodzie zadbamy tez o to, żeby wyświetlić informację o braku aktualności, gdy tablica "news" będzie pusta.
kod if (async_load[? "id"] == request) { // sprawdź czy to request utworzony przez tą instancję
// najpierw obsługa błędów
if (async_load[? "status"] < 0) {
if (async_load[? "status"] == -1 and async_load[? "http_status"] == 200) {
// timeout
// lub brak domeny w DNS
state = states.timeout;
} else {
state = states.error;
}
exit;
}
// teraz parsowanie, w wersji leniwej - bez okienka errorów mimo braku sprawdzania poprawności danych
try {
// spróbujmy sparsować
var _json = json_parse(async_load[? "result"]);
// teraz przelećmy newsy
for(var i = 0, n = array_length(_json.news); i < n; i++) {
// dodajemy nagłówek, treśc i datę (liczba)
content += "> " + _json.news[i].title + "\n\n";
content += _json.news[i].content + "\n\n";
content += date_date_string(_json.news[i].date);
// dodajmy poziomą linię za każdym "newsem"
if ( i < n-1) {
// ale nie dodawaj tego fragmentu za ostatnim wpisem
content += "\n----------\n";
}
}
// jeśli nie ma żadnych elementów w _json.news
if (n == 0) {
content = "Brak aktualności.";
}
state = states.succes; // na sam koniec, gdy wiemy, że nie poleci żaden exception
} catch (e) {
state = states.error; // jakikolwiek błąd
// można też dodać:
// show_debug_message(e)
// aby zobaczyć w której linijce nastąpił problem
}
}
// async_load jest zwalniane automatycznie
I to już w zasadzie wszystko.
Wskazówka:
Powyższy kod korzysta z zapisu async_load[? "klucz"] zamiast [kbd]ds_map_find_value(async_load, "klucz"). To tzw. akcesor, więcej o nich przeczytasz w artykule: [url=/index.php?czytajart=102]Akcesory - listy, mapy, gridy, structy, tablice[/url] .
Obsługa błędów
Można oczywiście sprawdzić jeszcze, jak działa obsługa błędów i czy faktycznie nie będzie typowych dla GMa ekranów "Code error" z nielubianym przyciskiem "Abort".
Wystarczy tylko zmienić domenę z gmclan.org na np. gmclanyyyy.org, albo nazwę pliku z example_news.json na example_news.jsonyyyy - i potwierdzimy działanie naszego kodu i poprawną obsługę błedów. Można też pokusić się o wyłączenie wifi.
Wskazówka:
Zawsze sprawdzajmy w ten sposób nasz kod dotyczący zapytań po sieci - wyłączając wifi, lub sprawdzając błędny adres - aby miec pewnośc, ze gra nie wysypie się naszym graczom przez drobną pomyłkę.
Gotowy przykład do pobraniaPrzykład stworzony w GM 2022.9.0 do pobrania tutaj:
gmclan.org/.../gmclan_example_ajax_news.yyz
Wyobraźmy sobie więc, że jakieś dane chcemy umieszczać na własnym serwerze, aby gra mogła dynamicznie je pobrać i je wyświetlić, lub nawet dostosować do nich gameplay.
Nie jest to wcale trudne i wymaga dosłownie kilkunastu minut roboty.
Tworzenie zapytania - Create
Stwórzmy nowy projekt, a w nim jeden obiekt. Będzie się on składał z zaledwie trzech zmiennych, oraz definicji enum którą dla ułatwienia tutaj wrzuciłem - warto jednak pamiętać, że enumy są globalne i podmieniane są w trakcie kompilacji na kolejne liczby rzeczywiste, więc ten fragment kodu tak naprawdę wyparuje.
Statusy pobierania danych będą 4 - początkowy, czyli wczytywanie, sukces, oraz timeout i błąd. Poza tym, potrzebujemy zmiennej do które zapiszemy pobrane przez nas dane (content), obecnego statusu (state) i identyfikatora zapytania (request), który rozpocznie zapytanie pod wskazany przez nas adres.
kod // statusy - otrzymają kolejne wartości 0,1,2,...
enum states {
loading,
succes,
timeout,
error
}
// zmienne
content = ""; // treśc którą wyświetlimy
state = states.loading; // ustawmy domyślny status
request = http_get("gmclan.org/.../example_news.json"); // rozpocznij zapytanie http
Ot i 1/3 projektu za nami!
Wyświetlanie wyniku - Draw
Jak już wspomniałem, mamy 4 statusy - zrobimy teraz ich wyświetlanie. Wiemy już, że każdy status poza success oznacza, że albo zapytanie trwa, albo coś nie wyszło. Obsłużmy więc pozostałe 3 osobno i wyświetlajmy krótki komunikat. Oczywiście, w samej grze być może chcielibyśmy wyświetlać jakiś animowany sprite robiący za pasek wczytywania. Nie zapomnijmy też stworzyć fonta, który będzie zawierał polskie znaki diakrytyczne - ja swój nazwałem
fnt_default
.Wskazówka:
Domyślny font w GameMaker nie wyświetla polskich znaków. Musimy więc dodać nowy font, kliknąć "Add", a następnie wpisać
ąćęłńóśżźĄĆĘŁŃÓŚŻŹ
i kliknąć "Add range".kod // ustawmy font, zawierający polskie znaki
draw_set_font(fnt_default);
if (state != states.succes) {
// jeśli wczytujemy lub jest błąd
var msg = "Wczytywanie...";
switch (state) {
case states.error:
msg = "Błąd pobierania.";
break;
case states.timeout:
msg = "Nie udało się wczytać adresu.";
break;
}
draw_text(10, 10, msg);
} else {
// wypiszmy newsy
draw_text(10, 10, content);
}
Obsługa zapytania - Async: HTTP
Teraz musimy obsłużyć moment, w którym GM wczytał (lub nie) dane z naszego adresu. Wykorzystamy event asynchroniczny HTTP.
Wskazówka:
Należy pamiętać, że jeśli więcej niż jeden obiekt ma wydarzenie Async HTTP, to w momencie gdy nasze zapytanie dobiegnie końca, zdarzenie to odpali się we wszystkich instancjach wszystkich obiektów bez względu czy to one rozpoczęły dane zapytanie. Właśnie dlatego przypisaliśmy je do zmiennej
request
aby dostać unikalne ID i odróżnić je od ewentualnych innychWskazówka:
Więcej o evencie Async HTTP znajdziecie w dokumetnacji: manual.yoyogames.com/.../HTTP.htm .
Dla nas najważniejsza informacją jest, że w tym evencie dostajemy specjalną, wygenerowaną tymczasowo przez GMa ds_mapę o nazwie
async_load
. Jej zaletą jest to, że zostaje ona przez GM automatycznie usunięta więc wyjątkowo nie musimy pamiętać o jej usuwaniu.Interesują nas jej trzy klucze:
- id - które zawiera identyfikator requestu który odpalił ten event. W naszej grze request jest jeden, ale "na przyszłość" uczymy się pisać bezpieczniejszy kod
- status - jeśli jest on mniejszy niż 0, to wystąpił błąd
- result - tekst który zwróciła witryna, bez nagłówków
W pierwszej kolejności zajmiemy się osbługą błędów (status < 0) i za pomocą exit zaprzestaniemy dalszego wykonywania kodu.
W drugim kroku spróbujemy sparsować JSONa którego otrzymaliśmy i wyświetlić dane.
Co jednak, gdy natrafimy na błąd serwera (np. przestanie on działać, dostaniemy 404, zmieni się struktura JSONa)? Należałoby po kolei sprawdzać, czy json się sparsował, czy zawiera wymagane przez nas dane itd.
GameMaker od wersji 2.3 (oraz w wersjach 2021+) oferuje jednak mechanizm dzięki któremu możemy sobie pozwolić być leniwymi i totalnie olać to sprawdzanie. Dzięki try-catch będziemy przetwarzać dane, a jeśli pojawi się jakiś błąd, uznajemy, że JSON nie jest poprawny i ustawiamy "state" na "error".
Wskazówka:
Korzystając z try-catch, nie dostaniemy okienka z błędem i gra będzie działać dalej, ale z drugiej strony przerwie wykonywanie kodu wewnątrz try{} przy pierwszym błędzie. Jeśli wiemy, że jakaś zmienna może, ale nie musi istnieć, można skorzystać z funkcji is_undefined() lub z
var wynik = niekoniecznie_istniejaca.wartosc ?? wartosc_domyslna;
, co też nie powoduje wyświetlenia komunikatu błędu, ani przejścia do catch{}Nasz przykładowy JSON znajdziecie tutaj:
gmclan.org/upload/gm/examples/example_news.json
Zawiera on pod własnością "news" tablicę, która składa się z własności title, content, date. Wiemy więc, że w danych jakie otrzymamy po sparsowaniu JSONa, będzie własność
.news
, w której jest tablica ze strukturami, mającymi te trzy własności. Za pomocą pętli for możemy więc przypisać je sobie do zmiennej content, która wyświetli listę newsów.Wskazówka:
Wartości w zmiennej date wygenerowałem wcześniej za pomocą
date_create_datetime
Wskazówka:Pętle for nie dopuszczają przecinków w swojej składni, ale wykorzystujemy fakt, że pozwala na taką składnię konstrukcja var.
W kodzie zadbamy tez o to, żeby wyświetlić informację o braku aktualności, gdy tablica "news" będzie pusta.
kod if (async_load[? "id"] == request) { // sprawdź czy to request utworzony przez tą instancję
// najpierw obsługa błędów
if (async_load[? "status"] < 0) {
if (async_load[? "status"] == -1 and async_load[? "http_status"] == 200) {
// timeout
// lub brak domeny w DNS
state = states.timeout;
} else {
state = states.error;
}
exit;
}
// teraz parsowanie, w wersji leniwej - bez okienka errorów mimo braku sprawdzania poprawności danych
try {
// spróbujmy sparsować
var _json = json_parse(async_load[? "result"]);
// teraz przelećmy newsy
for(var i = 0, n = array_length(_json.news); i < n; i++) {
// dodajemy nagłówek, treśc i datę (liczba)
content += "> " + _json.news[i].title + "\n\n";
content += _json.news[i].content + "\n\n";
content += date_date_string(_json.news[i].date);
// dodajmy poziomą linię za każdym "newsem"
if ( i < n-1) {
// ale nie dodawaj tego fragmentu za ostatnim wpisem
content += "\n----------\n";
}
}
// jeśli nie ma żadnych elementów w _json.news
if (n == 0) {
content = "Brak aktualności.";
}
state = states.succes; // na sam koniec, gdy wiemy, że nie poleci żaden exception
} catch (e) {
state = states.error; // jakikolwiek błąd
// można też dodać:
// show_debug_message(e)
// aby zobaczyć w której linijce nastąpił problem
}
}
// async_load jest zwalniane automatycznie
I to już w zasadzie wszystko.
Wskazówka:
Powyższy kod korzysta z zapisu async_load[? "klucz"] zamiast [kbd]ds_map_find_value(async_load, "klucz"). To tzw. akcesor, więcej o nich przeczytasz w artykule: [url=/index.php?czytajart=102]Akcesory - listy, mapy, gridy, structy, tablice[/url] .
Obsługa błędów
Można oczywiście sprawdzić jeszcze, jak działa obsługa błędów i czy faktycznie nie będzie typowych dla GMa ekranów "Code error" z nielubianym przyciskiem "Abort".
Wystarczy tylko zmienić domenę z gmclan.org na np. gmclanyyyy.org, albo nazwę pliku z example_news.json na example_news.jsonyyyy - i potwierdzimy działanie naszego kodu i poprawną obsługę błedów. Można też pokusić się o wyłączenie wifi.
Wskazówka:
Zawsze sprawdzajmy w ten sposób nasz kod dotyczący zapytań po sieci - wyłączając wifi, lub sprawdzając błędny adres - aby miec pewnośc, ze gra nie wysypie się naszym graczom przez drobną pomyłkę.
Gotowy przykład do pobraniaPrzykład stworzony w GM 2022.9.0 do pobrania tutaj:
gmclan.org/.../gmclan_example_ajax_news.yyz