Trwa konkurs "Ogól naczelnego", w którym codziennie możecie wygrać najnowsze maszynki systemowe Hydro Connect 5 marki Wilkinson Sword.

Więcej informacji
Strona używa cookies (ciasteczek). Dowiedz się więcej o celu ich używania i zmianach ustawień. Korzystając ze strony wyrażasz zgodę na używanie cookies, zgodnie z aktualnymi ustawieniami przeglądarki.    X

Portowanie gier z C/C++ do przeglądarki

Ten krótki wpis będzie traktować o przenoszeniu gier napisanych dla GNU/Linux do przeglądarek. Tak przerobioną grę można potem wrzucić na serwer i grać niezależnie od systemu operacyjnego. Wszystko, czego potrzebujemy, to nowoczesna przeglądarka internetowa. Co do przeniesienia, to potrzebujemy zainstalować emscripten(kroku nie opiszę), kod źródłowy w C. System budowania gry musi korzystać z pliku ./autogen, configure lub CMakeList.txt. Potrzeba też trochę samozaparcia.

Mozilla jakiś czas temu(jeszcze przed ogłoszeniem przez Google tworzenia NaCl) ogłosiła chęć stworzenia Posiksowej maszyny wirtualnej dla przeglądarek. Pomysł spotkał się z falą krytyki, że to zagraża neutralności sieci. Mozilla jednak zrealizowała swój plan, co pozwala niektóre programy z GNU/Linuksa przenosić do przeglądarki.

Trochę teorii

Emscripten to skrośny kompilator, który przenosi kod napisany dla systemu GNU/Linux do ASMJS i ewentualnie dodatkowo WebAssembly. Oba języki są wynalazkiem Mozilli, zarówno jak emscripten. Właściwie, to ASMJS nie jest całkiem odrębnym językiem, a podzbiorem JavaScript. WebAssembly to coś w stylu bytecodu Javy i jest on interpretowany przez nowoczesne przeglądarki, a starszym możemy zaserwować interpreter napisany w JavaScripcie. WebAssembly właściwie samemu nic nie może - może jedynie wykonywać obliczenia, zapisywać coś do pamięci, odczytywać coś z niej i wywoływać procedury/funkcje JavaScriptowe, jednak używa się WebAssembly w celu przyśpieszenia ładowania stron, a można także go użyć do ukrycia pewnych rzeczy. Myślę jednak, że pomimo możliwości użycia WebAssembly do ukrycia pewnych rzeczy, to nie jest to złem z powodu pracy w sandboksie i typowo obliczeniowej natury.

Emscripten dostarcza między innymi:

  • emcc - kompilator
  • emconfigure - tym narzędziem wywołujemy autogen, configure, a niekiedy trzeba nim uruchomić także make
  • emmake - tym narzędziem uruchamiamy make, make install i tym podobne

Zaczynamy

Pierwsze, co nam jest potrzebne, to kod źródłowy naszej gry. Możemy przenosić własną grę lub poszukać innej. Dobrymi grami są gry bazujące na OpenGL, SDL2, SDL. Trzeba jednak nadmienić, że emscriptowe wersje wymienionych bibliotek nie są pełne, więc nie wszystkie gry będzie dało się prosto przenieść. Kolejnym problemem jest natura przeglądarki. Większość gier korzysta z nieskończonym pętli, a przeglądarki nie mogą na takie zachowanie pozwolić. Dlatego każdą procedurę z pętlą nieskończoną przerabiamy na:
  1. Inicjalizację
  2. Pętlę właściwą
Dodatkowo deklaracje wszystkich zmiennych z procedury zawierającej pętlę należy umieścić w specjalnej strukturze.
Do inicjalizacji dodajemy (na samym początku) kod odpowiedzialny za utworzenie nowej struktury, naszego nowego typu, w pamięci, potem dodajemy cały kod przed pętlą niekończoną i zwracamy z tej funkcji utworzoną strukturę. Aha... Każde odwołanie się do zmiennej lokalnej z przerabianej procedury trzeba przerobić na odwołanie się do pola utworzonej struktury.
Do procedury pętli właściwej umieszczamy całe ciało pętli. Następnie przerabiamy każdy break na
np: cancel_main_loop(1); return; cancel_main_loop(1) jest tylko przykładem wyciągniętym z gry, którą przeniosłem, a mianowicie 7kaa. Anuluje to wywołanie naszej pętli i zwalnia pamięć naszej struktury(jakbym w ramach parametru przekazał 0, zamiast 1, to pamięć nie zostałaby zwolniona).

Obsługa pętli

Teraz bardzo ważny fragment - zajmuje się on odpowiednim neutralizowaniem minusów webowej natury naszej gry. Jest to prawdziwa pętla, która wywołuje pętlę właściwą, a tą prawdziwą pętlę wywołuje przeglądarka. Oto przykład: #include <emscripten.h> #include <OBOX.h> #include "loop.h" struct loop_information *main_loop; void set_main_loop( void (*iterator)(void*), // single step of main loop void *data, // Data passed to iterator void (*destructor)(void*) // will remove data (if present) ) { if (NULL == main_loop) { main_loop = (struct loop_information*) malloc(sizeof(*main_loop)); } else if (NULL != main_loop->destructor) { main_loop->destructor(main_loop->data); } main_loop->iterator = iterator; main_loop->data = data; main_loop->destructor = destructor; } void push_loop( void (*iterator)(void*), // single step of main loop void *data, // Data passed to iterator void (*destructor)(void*) // will remove data (if present) ) { struct loop_information *nloop = (struct loop_information*) malloc(sizeof(*nloop)); nloop->iterator = iterator; nloop->data = data; nloop->destructor = destructor; nloop->prev = main_loop; main_loop = nloop; } void prepend_each_loop( void (*iterator)(void*), // single step of main loop void *data, // Data passed to iterator void (*destructor)(void*) // will remove data (if present) ) { struct loop_information **pointer = &main_loop; struct loop_information *nloop = (struct loop_information*) malloc(sizeof(*nloop)); nloop->iterator = iterator; nloop->data = data; nloop->destructor = destructor; nloop->prev = NULL; while (NULL != *pointer) { pointer = &(*pointer)->prev; } *pointer = nloop; } void disable_main_loop() { main_loop->iterator = NULL; main_loop->data = NULL; main_loop->destructor = NULL; } void cancel_main_loop(char destroy) { struct loop_information *ploop; if (!main_loop) { SDL_ShowSimpleMessageBox( SDL_MESSAGEBOX_ERROR, "Seven Kingdoms - Runtime Error", "Main loop is NULL!", NULL ); return; } ploop = main_loop->prev; if (destroy) { if (main_loop->destructor) { main_loop->destructor(main_loop->data); } else { // TODO: Causing error //free(main_loop->data); } } free(main_loop); main_loop = ploop; } void browser_main_loop(void *data) { if (NULL == main_loop->iterator) { SDL_ShowSimpleMessageBox( SDL_MESSAGEBOX_ERROR, "Seven Kingdoms - Runtime Error", "Main loop is NULL!", NULL ); } try { main_loop->iterator(main_loop->data); } catch (GoToLoop) { } } void *get_destructor_for_current_loop(void) { return (void*) main_loop->destructor; } void wait_data_to_be_synced(struct data_sync_callback *mesh) { #if 1 EM_ASM_ARGS({ if (Module.synced == 1) { Module.Runtime.dynCall('vi', $0, [ $1 ]); } }, mesh->callback, mesh->data); #endif } Ważnym elementem jest wait_data_to_be_synced. Jeżeli chcemy móc np. zapisywać stany gry, to musimy synchronizować bazę danych przeglądarki z tym, co mamy w pamięci przeglądarki, po każdym zapisie, a także przy uruchomieniu. Do tego powrócę później.
Innym elementem jest browser_main_loop. Ta funkcja wywołuje naszą pętlę. Wywołanie następuje w bloku try .. catch, gdyż możemy odkładać na stos pętli, gdy uruchomiona jest inna pętla. Gdyby nie throw po odłożeniu elementu na stos, to wykonałyby się elementy zaraz po wyjściu z kodu inicjalizatora.

Prepend_each_loop jest potrzebne podczas inicjalizacji programu. Jeżeli chcemy wykonać oczekiwanie na zsynchronizowanie danych, to odkładamy każde takie oczekiwanie, a także pierwszą(główną) pętlę programu, za pomocą tej funkcji,
Push_loop działa podobnie, jak prepend_each_loop, lecz odkłada element na górę stosu, a więc odłożona pętla zostanie wykonana zaraz po wyjściu z iteracji obecnej pętli. throw GoToLoop();

Co zrobić z kodem po pętli gry?

Jeżeli mamy na myśli główny poziom pętli gry, to nic - po zamknięciu karty, przeglądarka zwolni zasoby. Co jednak z pozostałym pętlami? Tutaj wykorzystujemy push_loop i to, co mamy po pętli przenosimy do nowej funkcji. Push_loop zostanie omówiona potem. Ważnym jest, by nie wywoływać cancel_main_loop z parametrem 1 przed return w pętli właściwej, bo to zwolni nam naszą strukturę.

System plików

Jeżeli nasza gra ma jakieś dane, które wczytuje, to musimy zadbać o ich załadowanie przez przeglądarkę. Najprostszym sposobem jest użycie flagi --preload-file=<katalog lub plik>, przy tworzeniu pliku html przez emcc. Jeżeli chcemy coś zapisywać, co ma przetrwać sesję, to musimy zamontować odpowiedni system plików. To, jak tego dokonać: EM_ASM({ FS.mount(IDBFS,{},"/7kaa-home"); Module.synced = 0; FS.syncfs(true, function(error) { assert(!error); Module.synced = 1; }); }); Makro EM_ASM pozwala nam na robienie wstawek JavaScriptowych w kodzie C. W powyższym przykładzie jedynie montujemy nasz system plików. Pierwszy parametr przekazany do FS.syncfs mówi, czy dane mają być odczytane(true) lub zapisane(false).

Oczekiwanie na synchronizację

Oto kod odpowiedzialny za synchronizację: void wait_data_to_be_synced(struct data_sync_callback *mesh) { #if 1 EM_ASM_ARGS({ if (Module.synced == 1) { Module.Runtime.dynCall('vi', $0, [ $1 ]); } }, mesh->callback, mesh->data); #endif } Przedstawiłem go już ponownie. EM_ASM_ARGS jest makrem C, które pozwala na wstawienie kodu JavaScript, do którego przekażemy parametry. Struktura data_sync_callback zawiera wskaźnik na procedurę, która ma zostać wywołana, czyli callback, a także dane, które trzeba przekazać (data), Wait_data_to_be_synced musi być wywołana za pomocą funkcji browser_main_loop. Ważne jest, by nasza procedura przekazana za pomocą pola callback wywoływała cancel_main_loop(0) lub cancel_main_loop(1). Podsumowując, wykonujemy, po zapisie stanu gry. struct data_sync_callback *ds = (struct data_sync_callback*) malloc(sizeof(*ds)); ds->callback = choćby_tylko_cancel_main_loop ds->data = "Data synced"; push_loop((void(*)(void*))wait_data_to_be_synced, (void*) ds, NULL); i void choćby_tylko_cancel_main_loop(char *status) { puts(status); cancel_main_loop(0); }

Teraz pora, by omówić Module.Runtime.dynCall. Pierwszy argument, to sygnatura funkcji, kolejny to adres funkcji, a następnie tablica argumentów. Pierwszą literą w sygnaturze, to typ zwracanej wartości. W naszym wypadku jest to void('v'). Kolejnymi są typy przyjmowanych argumentów.W naszym wskaźnik, czyli ('i' od integer; tak - wskaźnik w JavaScripcie to to samo, co liczba całkowita).

Kompilacja

W większości przypadków emconfigure ./configure emmake make mv ścieżka_do_pliku_końcowego ścieżka_do_pliku_końcowego.bc emcc ścieżka_do_pliku_końcowego.bc -o program.html Emscripten sam wygeneruje dla nas stronę internetową.

Podsumowanie

Pomijając fakt, że możemy natknąć się na braki niektórych funkcji, np. biblioteki SDL lub bibliotek, to przenoszenie programów na przeglądarkę z C/C++ nie jest bardzo trudne. Największym problemem tak naprawdę są wadliwe skrypty konfiguracyjne i konieczność owijania pętli w pętlę przeglądarki, jak i brak możliwości wykorzystywania wielu, zagnieżdżonych pętli gry, co rozwiązałem przez moją implementację pętli (wywołania push_loop, wykorzystanie throw GoToLoop(), itd.). Do emscripten zostało przeniesionych dużo interpreterów języków programowania, w tym maszyna wirtualna Javy, Pythona, itd. Został przeniesiony także DosBox. Jest mnóstwo ciekawych projektów, a więcej czeka na przeniesienie.

Jeden z moich projektów to battleof7(przeniesiona wersja 7kaa), które jest dostępny na sourceforgu. 

linux internet programowanie

Komentarze

0 nowych
GNUser   6 #1 30.04.2017 19:46

Świetny poradnik. Tylko czy pod względem wydajności jest sens portować gry/programy napisane w C? Jak się ma wydajność gry w C dla przeglądarki z np. grą w Lispie także w przeglądarce?

  #2 30.04.2017 20:02

no właśnie autorze czy jest jakiś sens tego? Nie szkoda czasu, pieniążków? Czy to taka sztuka dla sztuki?

GNUser   6 #3 30.04.2017 21:08

@kentaro: Nie chodziło mi o krytykę jego wpisu. Po prostu się zastanawiam jaka jest wydajność takiej gry, bo przecież języki C i C++ nie są z natury webowe.

nintyfan   11 #4 30.04.2017 21:19

@GNUser: Nie za bardzo się orientuję w tych kwestiach, gdyż 7kaa jest w zasadzie drugą grą, którą przeniosłem. Na moim komputerze działa płynnie. Mozilla obiecuje, że WebAssembly zapewnia niemal natywną wydajność, lecz programy w Javie też miały być szybkie, a JavaScript także jest kompilowany do kodu natywnego. Różnicą między WebAssembly, a AsmJS, powinna być odczuwalna głównie podczas ładowania, bo łatwiej przetłumaczyć kod WebAssembly na natywny niż kod w JavaScripcie.

Trzeba nadmienić, że AsmJS zawiera nawet wskaźniki, by emulować mechanizmy języka C/C++. Skorzystaliśmy z tego w Module.Runtime.dynCall. Skoro są wskaźniki, a więc powinno dać się zrealizować takie same algorytmy, co w C.

nintyfan   11 #5 30.04.2017 21:23

@kentaro: To nie taka sztuka dla sztuki, gdyż BattleOf7(webowa wersja 7kaa) jest chyba najlepszą, w tym momencie, strategią czasu rzeczywistego w html5. Mam zamiar to wykorzystać do zarabiania pieniędzy, jak kody w grze sieciowej za opłatą, możliwość wykorzystania własnych avatarów czy nazw jednostek w rozgrywkach sieciowych, a także przetrzymywanie zapisów stanu gry w chmurze. Zanim jednak zacznę, to muszę przeportować więcej gier.

Tak na marginesie, to jak wspomniałem, BattleOf7 jest chyba najlepszą strategią czasu rzeczywistego na przeglądarki, a więc mogę sobie to zapisać w portfolio.

GNUser   6 #6 30.04.2017 23:17

@nintyfan: Skoro jest to przenoszenie kodu w C/C++ do assemblera to w teorii powinien taki kod być nawet bardziej wydajny. Podejrzewam, że w dużej mierze wydajność przeportowanego w taki sposób kodu, zależy od jego jakości w pierwotnym języku.

Autor edytował komentarz w dniu: 01.05.2017 04:26
mikolaj_s   15 #7 01.05.2017 11:08

@GNUser: WebAssembly to nie to samo co zwykły assembler. To odmiana bytecode działająca w przeglądarce. Zasadniczo ma działać w taki sposób, że pisze się program w dowolnym języku programowania, który ma kompilator do webassembly i wytwarzany jest bytecode, który wykorzystuje przeglądarka. Nie ma co liczyć, że będzie tak samo szybki jak kod C. Jest realna szansa, że powinien być równie szybki co bytecode JVM.

PS. Ze strony asm.js: (http://asmjs.org/faq.html)
Q. What kind of performance benefits can I expect to get with asm.js?
A. It's early to say, but our preliminary benchmarks of C programs compiled to asm.js are usually within a factor of 2 slowdown over native compilation with clang. We will publish more benchmarks as we collect them.

Autor edytował komentarz w dniu: 01.05.2017 11:24
GNUser   6 #8 01.05.2017 15:59

@mikolaj_s: Dzięki za wyjaśnienie.

  #9 08.05.2017 17:51

pokaz jakis prawdziwy przyklad np. z SDL2

Gratulacje!

znalezione maszynki:

Twój czas:

Ogól Naczelnego!
Znalazłeś(aś) 10 maszynek Wilkinson Sword
oraz ogoliłaś naszego naczelnego!
Przejdź do rankingu
Podpowiedź: Przyciśnij lewy przycisk myszki i poruszaj nią, aby ogolić brodę.