Blog (75)
Komentarze (5.3k)
Recenzje (0)

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

@nintyfanPortowanie gier z C/C++ do przeglądarki30.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.

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:

[code=C++] cancel_main_loop(1); return; [/code]

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:

[code=C] #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 }

[/code]

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.

[code=C] throw GoToLoop(); [/code]

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

[code=C/JavaScript] EM_ASM({ FS.mount(IDBFS,{},"/7kaa-home"); Module.synced = 0; FS.syncfs(true, function(error) { assert(!error); Module.synced = 1; }); }); [/code]

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

[code=C] 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 } [/code]

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.

[code=C] 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); [/code]

i

[code=C] void choćby_tylko_cancel_main_loop(char *status) { puts(status); cancel_main_loop(0); } [/code]

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.

Szanowna Użytkowniczko! Szanowny Użytkowniku!
×
Aby dalej móc dostarczać coraz lepsze materiały redakcyjne i udostępniać coraz lepsze usługi, potrzebujemy zgody na dopasowanie treści marketingowych do Twojego zachowania. Twoje dane są u nas bezpieczne, a zgodę możesz wycofać w każdej chwili na podstronie polityka prywatności.

Kliknij "PRZECHODZĘ DO SERWISU" lub na symbol "X" w górnym rogu tej planszy, jeżeli zgadzasz się na przetwarzanie przez Wirtualną Polskę i naszych Zaufanych Partnerów Twoich danych osobowych, zbieranych w ramach korzystania przez Ciebie z usług, portali i serwisów internetowych Wirtualnej Polski (w tym danych zapisywanych w plikach cookies) w celach marketingowych realizowanych na zlecenie naszych Zaufanych Partnerów. Jeśli nie zgadzasz się na przetwarzanie Twoich danych osobowych skorzystaj z ustawień w polityce prywatności. Zgoda jest dobrowolna i możesz ją w dowolnym momencie wycofać zmieniając ustawienia w polityce prywatności (w której znajdziesz odpowiedzi na wszystkie pytania związane z przetwarzaniem Twoich danych osobowych).

Od 25 maja 2018 roku obowiązuje Rozporządzenie Parlamentu Europejskiego i Rady (UE) 2016/679 (określane jako "RODO"). W związku z tym chcielibyśmy poinformować o przetwarzaniu Twoich danych oraz zasadach, na jakich odbywa się to po dniu 25 maja 2018 roku.

Kto będzie administratorem Twoich danych?

Administratorami Twoich danych będzie Wirtualna Polska Media Spółka Akcyjna z siedzibą w Warszawie, oraz pozostałe spółki z grupy Wirtualna Polska, jak również nasi Zaufani Partnerzy, z którymi stale współpracujemy. Szczegółowe informacje dotyczące administratorów znajdują się w polityce prywatności.

O jakich danych mówimy?

Chodzi o dane osobowe, które są zbierane w ramach korzystania przez Ciebie z naszych usług, portali i serwisów internetowych udostępnianych przez Wirtualną Polskę, w tym zapisywanych w plikach cookies, które są instalowane na naszych stronach przez Wirtualną Polskę oraz naszych Zaufanych Partnerów.

Dlaczego chcemy przetwarzać Twoje dane?

Przetwarzamy je dostarczać coraz lepsze materiały redakcyjne, dopasować ich tematykę do Twoich zainteresowań, tworzyć portale i serwisy internetowe, z których będziesz korzystać z przyjemnością, zapewniać większe bezpieczeństwo usług, udoskonalać nasze usługi i maksymalnie dopasować je do Twoich zainteresowań, pokazywać reklamy dopasowane do Twoich potrzeb. Szczegółowe informacje dotyczące celów przetwarzania Twoich danych znajdują się w polityce prywatności.

Komu możemy przekazać dane?

Twoje dane możemy przekazywać podmiotom przetwarzającym je na nasze zlecenie oraz podmiotom uprawnionym do uzyskania danych na podstawie obowiązującego prawa – oczywiście tylko, gdy wystąpią z żądaniem w oparciu o stosowną podstawę prawną.

Jakie masz prawa w stosunku do Twoich danych?

Masz prawo żądania dostępu, sprostowania, usunięcia lub ograniczenia przetwarzania danych. Możesz wycofać zgodę na przetwarzanie, zgłosić sprzeciw oraz skorzystać z innych praw wymienionych szczegółowo w polityce prywatności.

Jakie są podstawy prawne przetwarzania Twoich danych?

Podstawą prawną przetwarzania Twoich danych w celu świadczenia usług jest niezbędność do wykonania umów o ich świadczenie (tymi umowami są zazwyczaj regulaminy). Podstawą prawną przetwarzania danych w celu pomiarów statystycznych i marketingu własnego administratorów jest tzw. uzasadniony interes administratora. Przetwarzanie Twoich danych w celach marketingowych realizowanych przez Wirtualną Polskę na zlecenie Zaufanych Partnerów i bezpośrednio przez Zaufanych Partnerów będzie odbywać się na podstawie Twojej dobrowolnej zgody.