Blog (67)
Komentarze (2.6k)
Recenzje (0)

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

Strona główna@nintyfanPortowanie gier z C/C++ do przeglądarki
30.04.2017 13:43

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.

bDUJtvqv

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:

bDUJtvqB
  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:

cancelmainloop(1); return;

cancelmainloop(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:

bDUJtvqC
#include #include #include "loop.h"

struct loopinformation mainloop;

void setmainloop( void (iterator)(void), // single step of main loop void data, // Data passed to iterator void (destructor)(void) // will remove data (if present) ) { if (NULL == mainloop) { mainloop = (struct loopinformation) malloc(sizeof(mainloop)); } else if (NULL != mainloop->destructor) { mainloop->destructor(mainloop->data); } mainloop->iterator = iterator; mainloop->data = data; mainloop->destructor = destructor; }

void pushloop( void (iterator)(void), // single step of main loop void data, // Data passed to iterator void (destructor)(void) // will remove data (if present) ) { struct loopinformation nloop = (struct loopinformation) malloc(sizeof(nloop)); nloop->iterator = iterator; nloop->data = data; nloop->destructor = destructor; nloop->prev = mainloop; mainloop = nloop; }

bDUJtvqD

void prependeachloop( void (iterator)(void), // single step of main loop void data, // Data passed to iterator void (destructor)(void) // will remove data (if present) ) { struct loopinformation pointer = &mainloop; struct loopinformation nloop = (struct loopinformation) malloc(sizeof(nloop)); nloop->iterator = iterator; nloop->data = data; nloop->destructor = destructor; nloop->prev = NULL; while (NULL != pointer) { pointer = &(pointer)->prev; } pointer = nloop; }

void disablemainloop() { mainloop->iterator = NULL; mainloop->data = NULL; mainloop->destructor = NULL; }

void cancelmainloop(char destroy) { struct loopinformation ploop; if (!mainloop) { SDLShowSimpleMessageBox( SDLMESSAGEBOXERROR, "Seven Kingdoms - Runtime Error", "Main loop is NULL!", NULL ); return; } ploop = mainloop->prev; if (destroy) { if (mainloop->destructor) { mainloop->destructor(mainloop->data); } else { // TODO: Causing error //free(mainloop->data); } } free(mainloop); mainloop = ploop; }

void browsermainloop(void data) { if (NULL == mainloop->iterator) { SDLShowSimpleMessageBox( SDLMESSAGEBOXERROR, "Seven Kingdoms - Runtime Error", "Main loop is NULL!", NULL ); } try { mainloop->iterator(mainloop->data); } catch (GoToLoop) { } }

bDUJtvqE

void getdestructorforcurrentloop(void) { return (void) mainloop->destructor; }

void waitdatatobesynced(struct datasynccallback mesh) { #if 1 EMASMARGS({ if (Module.synced == 1) { Module.Runtime.dynCall('vi', $0, $1 ); } }, mesh->callback, mesh->data); #endif }

/code

Ważnym elementem jest waitdatatobesynced. 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 browsermainloop. 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.

Prependeachloop 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, Pushloop działa podobnie, jak prependeachloop, 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 pushloop i to, co mamy po pętli przenosimy do nowej funkcji. Pushloop zostanie omówiona potem. Ważnym jest, by nie wywoływać cancelmainloop 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=, 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ć:

EMASM({FS.mount(IDBFS,{},"/7kaa-home");  Module.synced = 0; FS.syncfs(true, function(error) { assert(!error); Module.synced = 1;  });});

Makro EMASM 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. EMASMARGS jest makrem C, które pozwala na wstawienie kodu JavaScript, do którego przekażemy parametry. Struktura datasynccallback zawiera wskaźnik na procedurę, która ma zostać wywołana, czyli callback, a także dane, które trzeba przekazać (data), Waitdatatobesynced musi być wywołana za pomocą funkcji browsermainloop. Ważne jest, by nasza procedura przekazana za pomocą pola callback wywoływała cancelmainloop(0) lub cancelmainloop(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_loopds->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 ./configureemmake makemv ścieżka_do_pliku_końcowego ścieżka_do_pliku_końcowego.bcemcc ś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 pushloop, 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.

bDUJtvrr