Autor Wątek: Ładowanie assetów z ekranem loading na platformie która eventuje onDraw (C++)  (Przeczytany 3634 razy)

Offline Shelim

  • Użytkownik
    • Homepage

# Lipiec 25, 2012, 00:56:33
Piszę sobie taki minisilnik 2D na platformy mobile (Android, iOS, itp.); W zasadzie większość już mam napisaną, pozostał problem z assetami. Podszedłem jak zawsze od tyłka strony, czyli najpierw zaprojektowałem sobie API a potem postanowiłem dorobić do tego rdzeń; Api wczytywania jest proste:

pBitmap foo; // counted pointer
pFont bar; // counted pointer
void LoadAssets()
{
foo = Load<Bitmap>("Foo.png");
bar = Load<Font>("Bar.font");
}
Funkcja LoadAsset jest jedną z pięciu globalnych funkcji do zaimplementowania przez grę - jeżeli ktoś chce szybkiego prototypu; Występuje też w klasie abstrakcyjnej State, dzięki której można zrobić bardziej zaawansowany system GameStateów z  ładowaniem niezbędnych assetów praktycznie z marszu.

So far so good, to rozwiązanie zostało już zaimplementowane i działa całkiem fajnie;

Problemem jest fakt że chciałbym mieć ekran loading z paskiem postępu. Zmodyfikowałem API o:
class MyLoadingScreen : public LoadingScreen
{
public:
void Initialize() { }
void Render(float progress, const char * currentfilename) { }
void Shutdown() { }
};

pBitmap foo; // counted pointer
pFont bar; // counted pointer
MyLoadingScreen loadingScreen // Dla globalnego LoadAsset to może być globalna zmienna, dla state'ów powinno to być pole prywatne
void LoadAssets()
{
SetLoadingScreen(&loadingScreen);
foo = Load<Bitmap>("Foo.png");
bar = Load<Font>("Bar.font");
}

Generalnie wydaje mi się że takie API jest bardzo praktyczne, można łatwo dopisać nowy typ zasobu specjalizując jedną funkcję szablonową, zasoby zostają odładowane z automatu gdy nie są już używane (licznik referencji zleci do zera), nie ma problemu że zasób zostanie odładowany gdy wciąż istnieją do niego referencje, zasoby z definicji nie będą miały cyklicznych referencji.

Problemem jest ten przeklęty ekran loading - na Androidzie nie mogę rysować kiedy chcę, dostaję event OnDrawFrame, w którym wywołuję kod gry. Co oznacza że pojawia się następujący callstack:
OnDrawFrame
EngineDraw // czas na załadowanie assetów? - tak
LoadAssets
Load<Bitmap>
LoadBinary<Bitmap> // po wykonaniu chcielibyśmy zaktualizować pasek postępu, co oznacza konieczność narysowanie nowej klatki... ale zaraz, już jesteśmy w OnDrawFrame ;/
Oczywiście po Load<Bitmap>("Foo.png") nie da się "przeskoczyć" do końca OnDrawFrame, zaczekać na kolejną klatkę i wrócić do Load<Font>("Bar.font") w funkcji LoadAsset

Przyszło mi do głowy parę rozwiązań problemu:
 + Kolejkowanie do wczytania - na bardzo upartego może być, jednak oznaczałoby że do momentu zakończenia LoadAsset żaden z assetów nie jest jeszcze załadowany (co np. uniemożliwiłoby wczytywanie assetów w zależności od contentu jednego z nich)
 + Jakieś sprytne makro które zastąpiłoby Load<T>
 + goto chyba nie przejdzie przy czymś takim, nie...?
 + Zmiana API - ale nie chciałbym stracić prostoty którą tu osiągnąłem...
 + Wychrzanić paski postępu na ekranie loading i zrobić po najmniejszej linii oporu: ekran "Please wait, loading..."
 + Inne - jakie?

Generalnie dawno nie trafiłem na tak durny problem projektowy przy tworzeniu API - mam nadzieję że ktoś bardziej przytomny ode mnie wpadnie na jakieś proste rozwiązanie którego nie widzę ;-)

Offline Mr. Spam

  • Miłośnik przetworów mięsnych

Offline Xender

  • Użytkownik

  • +1
# Lipiec 25, 2012, 02:10:54
Może przerzucić samo wczytywanie do dedykowanego wątku i dać opcje wczytywania asynchronicznego lub synchronicznego. W głównym wątku w OnDrawFrame pytać wątek roboczy o postęp.

A w ogóle to zaraz - jak działają te eventy? W pętli głównej musisz wywołać jakieś dispatch(), które wywołuje callbacki, czy na chama leci to jak przerwania/sygnały?
« Ostatnia zmiana: Lipiec 25, 2012, 02:15:11 wysłana przez olo16 »

Offline Shelim

  • Użytkownik
    • Homepage

# Lipiec 25, 2012, 02:47:30
O, to jest dobry pomysł z tym wątkiem! I nawet wyeliminuje znany paradoks, który mówi że jeżeli ładujesz dużo małych plików to najwolniejszą operacją jest odświeżanie paska postępu :D

Ad eventów, to jest tak że na Androidzie nie można pisać w czystym C++ (no, przynajmniej jeżeli celujesz w nieco starsze komórki) - musi być kawałek Javy który wywołuje natywne funkcje C++; A ten kawałek Javy działa tak że dostaje eventy od systemu ;)

Offline Xender

  • Użytkownik

# Lipiec 25, 2012, 02:56:08
A, czyli pętla główna jest w Javie, pobiera eventy i wywołuje callbacki?
Co do wielu małych plików to lepiej VFS ;)

Offline Shelim

  • Użytkownik
    • Homepage

# Lipiec 25, 2012, 03:16:05
Yep, korzystam z Physfs, wygodne :)

Offline Krzysiek K.

  • Redaktor
    • DevKK.net

# Lipiec 25, 2012, 09:06:13
Cytuj
O, to jest dobry pomysł z tym wątkiem!
Masz oczywiście świadomość, że w Androidzie funkcje OpenGL możesz wywoływać tylko z wątku, który utworzył kontekst?

Cytuj
I nawet wyeliminuje znany paradoks, który mówi że jeżeli ładujesz dużo małych plików to najwolniejszą operacją jest odświeżanie paska postępu :D
Nie paradoks, tylko brak umiejętności. ;)

Cytuj
Co do wielu małych plików to lepiej VFS ;)
Pod Androidem i tak masz wszystkie assety w zipie, więc siłą rzeczy masz VFS. ;)

Offline Interceptor

  • Użytkownik

# Lipiec 25, 2012, 11:57:07
Mozesz zrobic takie cos:

loadresource()
{
wczytywanie
tworzenie

...

glclearcolor
glviewport
rysowanie ekranu loading
glswapbuffers
}

Offline MrKaktus

  • Użytkownik

# Lipiec 25, 2012, 15:14:02
Słaby pomysł. Natura aplikacji w Androidzie niejako wymusza użycie wątków.

Offline Krzysiek K.

  • Redaktor
    • DevKK.net

# Lipiec 25, 2012, 15:44:44
Cytuj
Mozesz zrobic takie cos:
...
glswapbuffers
Nie możesz, bo w Androidzie nie masz swap buffers. Robi to za Ciebie system a Ty musisz tylko narysować ramkę na żądanie.

Cytuj
Słaby pomysł. Natura aplikacji w Androidzie niejako wymusza użycie wątków.
Tak. Przynajmniej jednego. Ale więcej to już niekoniecznie. ;)

Offline Shelim

  • Użytkownik
    • Homepage

  • +1
# Lipiec 25, 2012, 16:20:36
Głupi Android :P

No dobra, to zostało mi pójście po najmniejszej linii oporu, czyli walnięcie "Loading, please wait..."

Offline jelcynek

  • Użytkownik

# Lipiec 25, 2012, 16:58:28
Cytuj
po najmniejszej linii oporu

"po linii najmniejszego oporu"

Offline Xender

  • Użytkownik

# Lipiec 25, 2012, 18:00:18
Może jednak nie rezygnować z wątków tak szybko? Problem jest taki, że samo wczytywanie jest blokujące, a my w tym czasie chcemy coś wyświetlać - naturalne będzie zrobienie tego asynchronicznie. Zamiast jednej funkcji Load można zrobić 2 - asynchroniczną RequestLoad i blokującą EnsureLoaded (nadal szablony, ale z automatyczną dedukcją typu). Dlaczego tak? Dla ładowania np. tekstury trzeba: (1) wczytać plik z dysku i wykonać ewentualną konwersję / dekompresję / whatever, (2) przerzucić dane z bufora w pamięci do VRAMu wywołaniem OpenGL. Krzysiek K. słusznie zauważył, że to drugie musi być zrobione w głównym wątku, ale to pierwsze nadal może być asynchroniczne.

Dodatkowo za tym podejściem przemawia fakt, że chcemy jeden pasek postępu dla wszystkich zasobów, więc od samego początku ładowania trzeba znać łączną ich wielkość.

Proponowałbym taki przykład wykorzystania:

Texture foo; // counted pointer
Font bar; // counted pointer

class MyLoadingScreen : public LoadingScreen
{
public:
void Initialize()
{
RequestLoad(foo, "Foo.png");
RequestLoad(bar, "Bar.font");
}

void Render(float progress, const char * currentfilename) { ... }

void Shutdown()
{
EnsureLoaded(foo);
EnsureLoaded(bar);
}
};

RequestLoad zlecałoby wątkowi roboczemu wczytanie danego zasobu asynchronicznie (załadowanie pliku, konwersja danych...).

EnsureLoaded czekałoby na wątek roboczy (jeśli jeszcze nie skończył wczytywać) i wykonywałoby operacje, które muszą być synchroniczne (ładowanie tekstury do VRAMu).

Stan gry odpowiedzialny za ładowanie musiałby tylko wywołać loadingScreen.Initialize(), podczas przetwarzania eventu odpytywać wątek roboczy o postęp i wywołać loadingScreen.Render(), i jeśli otrzymałby informację, że asynchroniczna część roboty jest zakończona, wywoływałby loadingScreen.Shutdown(), w tym momencie przycinka paska ładowania na 100%, a potem zmiana stanu gry na kolejny lvl czy co tam miało być.
« Ostatnia zmiana: Lipiec 25, 2012, 18:01:51 wysłana przez olo16 »

Offline Krzysiek K.

  • Redaktor
    • DevKK.net

# Lipiec 25, 2012, 18:57:54
Cytuj
Krzysiek K. słusznie zauważył, że to drugie musi być zrobione w głównym wątku, ale to pierwsze nadal może być asynchroniczne.
Ale na szczęście nie musi. Dopóki nie masz tyle assetów, żeby ładowały się za każdym razem pół minuty, to nie masz się czym przejmować i każda kombinacja to czas stracony w niepotrzebnym miejscu. A na tych urządzeniach, gdzie rzeczywiście assety będą się długo ładowały i tak najprawdopodobniej będziesz miał procesor jednordzeniowy i wątki całe wielkie nic.

Offline Xender

  • Użytkownik

# Lipiec 25, 2012, 19:53:00
@up - dobra:

1. Zaproponuj lepszy system - statyczne "loading, please wait"? Chyba, żeby robić blokujące wczytywanie per plik, a między wywołaniami obsługiwać onDraw.
2. Procesor jednordzeniowy akurat bardzo wystarcza do wystarcza do jednoczesnego wykonywania zadania zorientowanego na I/O i zadania, które czeka na eventy od OSu...

Offline Krzysiek K.

  • Redaktor
    • DevKK.net

# Lipiec 25, 2012, 22:41:03
1. Ja w tej chwili nie robię nic - po prostu blokuję na czas ładowania. Pomyślę jak już będę miał skończoną grę w której to będzie problemem. ;)

2. Weź pod uwagę, że urządzenia androidowe z reguły są wyposażone w szybsze "dyski" od dysków twardych.