Tworzę grę fizyki z udziałem sztywnych ciał, w której gracze poruszają pionkami i częściami, aby rozwiązać zagadkę / mapę. Niezwykle ważnym aspektem gry jest to, że gdy gracze rozpoczynają symulację, działa tak samo wszędzie , niezależnie od systemu operacyjnego, procesora itp.
Jest dużo miejsca na złożoność, a symulacje mogą działać przez długi czas, dlatego ważne jest, aby silnik fizyki był całkowicie deterministyczny w odniesieniu do operacji zmiennoprzecinkowych, w przeciwnym razie może się wydawać, że rozwiązanie „rozwiązuje” na maszynie jednego gracza i „nie” na innym.
Jak mogę osiągnąć ten determinizm w mojej grze? Jestem gotów używać różnych frameworków i języków, w tym Javascript, C ++, Java, Python i C #.
Kusiło mnie Box2D (C ++) oraz jego odpowiedniki w innych językach, ponieważ wydaje się, że spełnia moje potrzeby, ale brakuje mu determinizmu zmiennoprzecinkowego, szczególnie w przypadku funkcji trygonometrycznych.
Najlepszą opcją, jaką do tej pory widziałem, jest Java Box2D (JBox2D). Wygląda na to, że podejmuje próbę determinizmu zmiennoprzecinkowego przy użyciu StrictMath
zamiast Math
wielu operacji, ale nie jest jasne, czy ten silnik zagwarantuje wszystko, czego potrzebuję, ponieważ jeszcze nie zbudowałem gry.
Czy można użyć lub zmodyfikować istniejący silnik, aby dostosować go do moich potrzeb? Czy też będę musiał samodzielnie zbudować silnik?
EDYCJA: Pomiń resztę tego postu, jeśli nie obchodzi Cię, dlaczego ktoś potrzebuje takiej precyzji. Osoby w komentarzach i odpowiedziach wydają się wierzyć, że szukam czegoś, czego nie powinienem, dlatego wyjaśnię, jak powinna działać gra.
Gracz otrzymuje układankę lub poziom, który zawiera przeszkody i cel. Początkowo symulacja nie jest uruchomiona. Następnie mogą użyć dostarczonych im elementów lub narzędzi do zbudowania maszyny. Po naciśnięciu przycisku Start rozpoczyna się symulacja i nie można już edytować maszyny. Jeśli maszyna rozwiąże mapę, gracz przekroczył poziom. W przeciwnym razie będą musieli nacisnąć stop, zmienić maszynę i spróbować ponownie.
Powodem, dla którego wszystko musi być deterministyczne, jest to, że planuję stworzyć kod, który zamapuje każdą maszynę (zestaw elementów i narzędzi, które próbują rozwiązać poziom) na plik xml lub json, rejestrując pozycję, rozmiar i obrót każdego elementu. Gracze będą wtedy mogli dzielić się rozwiązaniami (które są reprezentowane przez te pliki), aby mogli weryfikować rozwiązania, uczyć się od siebie nawzajem, organizować konkursy, współpracować itp. Oczywiście większość rozwiązań, szczególnie proste lub szybkie tych, nie będzie dotknięty brakiem determinizmu. Ale powolne lub złożone projekty, które rozwiązują naprawdę trudne poziomy, mogą być tymi, które prawdopodobnie będą najbardziej interesujące i warte podzielenia się.
źródło
Odpowiedzi:
O obsłudze liczb zmiennoprzecinkowych w sposób deterministyczny
Zmienny punkt jest deterministyczny. Cóż, powinno być. To skomplikowane.
Istnieje mnóstwo literatury na temat liczb zmiennoprzecinkowych:
I jak są problematyczne:
Dla streszczenia. Przynajmniej w jednym wątku te same operacje z tymi samymi danymi, zachodzące w tej samej kolejności, powinny być deterministyczne. Dlatego możemy zacząć od martwienia się o dane wejściowe i zmiany kolejności.
Jednym z takich danych wejściowych, które powodują problemy, jest czas.
Przede wszystkim zawsze należy obliczyć ten sam czas. Nie mówię, żeby nie mierzyć czasu, mówię, że nie przejdziesz czasu do symulacji fizyki, ponieważ zmiany w czasie są źródłem hałasu w symulacji.
Dlaczego mierzysz czas, jeśli nie przekazujesz go do symulacji fizyki? Chcesz zmierzyć czas, który upłynął, aby wiedzieć, kiedy należy wywołać krok symulacji, i - zakładając, że korzystasz ze snu - ile czasu spać.
A zatem:
Teraz zmiana kolejności instrukcji.
Kompilator może zdecydować, że
f * a + b
jest to to samo, cob + f * a
jednak może mieć inny wynik. Może również skompilować się do fmadd lub może zdecydować, że weźmie wiele takich linii, które się to zdarzy, razem i napiszę je za pomocą SIMD lub innej optymalizacji, o której nie mogę teraz myśleć. I pamiętajmy, że chcemy, aby te same operacje odbywały się w tej samej kolejności, dlatego chcemy kontrolować, co się dzieje.I nie, użycie double nie uratuje cię.
Musisz się martwić kompilatorem i jego konfiguracją, w szczególności synchronizować liczby zmiennoprzecinkowe w sieci. Musisz uzyskać wersje, aby zgodzić się na to samo.
Prawdopodobnie pisanie zestawu byłoby idealne. W ten sposób decydujesz, którą operację wykonać. Może to jednak stanowić problem w przypadku obsługi wielu platform.
A zatem:
Przypadek liczb stałych
Ze względu na sposób, w jaki pływaki są reprezentowane w pamięci, duże wartości stracą precyzję. Przyczyną jest to, że utrzymywanie małych wartości (zacisk) łagodzi problem. Zatem nie ma wielkich prędkości i nie ma dużych pomieszczeń. Co oznacza również, że możesz używać dyskretnej fizyki, ponieważ masz mniejsze ryzyko tunelowania.
Z drugiej strony będą się kumulować małe błędy. Więc obetnij. Mam na myśli, skaluj i rzutuj na liczbę całkowitą. W ten sposób wiesz, że nic się nie buduje. Będą operacje, które możesz wykonać pozostając przy typie liczby całkowitej. Kiedy musisz wrócić do zmiennoprzecinkowego, rzucasz i cofasz skalowanie.
Uwaga Mówię skalę. Chodzi o to, że 1 jednostka będzie faktycznie reprezentowana jako potęga dwóch (na przykład 16384). Cokolwiek to jest, uczyń go stałym i używaj go. Zasadniczo używasz go jako stałego numeru punktu. W rzeczywistości, jeśli można użyć znacznie lepszych liczb stałych z pewnej niezawodnej biblioteki.
Mówię obcięty. Jeśli chodzi o problem z zaokrąglaniem, oznacza to, że nie możesz ufać ostatniej części wartości uzyskanej po obsadzie. Tak więc, zanim rzucasz skalę, aby uzyskać nieco więcej niż potrzebujesz, a następnie ją obetnij.
A zatem:
Czekaj, dlaczego potrzebujesz zmiennoprzecinkowego? Czy nie możesz pracować tylko z typem całkowitym? Och, racja. Trygonometria i promieniowanie. Możesz obliczyć tabele dla trygonometrii i radiacji i upiec je w swoim źródle. Lub możesz zaimplementować algorytmy użyte do obliczenia ich za pomocą liczb zmiennoprzecinkowych, z wyjątkiem użycia liczb stałych w zamian. Tak, musisz zrównoważyć pamięć, wydajność i precyzję. Jednak możesz trzymać się z dala od liczb zmiennoprzecinkowych i pozostać deterministycznym.
Czy wiesz, że robili takie rzeczy na oryginalnym PlayStation? Proszę spotkać mojego psa, łatki .
Nawiasem mówiąc, nie mówię, żebym nie używał zmiennoprzecinkowego grafiki. Tylko dla fizyki. Oczywiście, pozycje będą zależeć od fizyki. Jednak, jak wiadomo, zderzacz nie musi pasować do modelu. Nie chcemy widzieć wyników obcinania modeli.
Dlatego: UŻYWAJ STAŁYCH NUMERÓW PUNKTOWYCH.
Żeby było jasne, jeśli możesz użyć kompilatora, który pozwala ci określić, jak działają zmiennoprzecinkowe, i to ci wystarczy, możesz to zrobić. To nie zawsze jest opcja. Poza tym robimy to dla determinizmu. Stałe numery punktów nie oznaczają, że nie ma błędów, w końcu mają ograniczoną precyzję.
Nie sądzę, aby „ustalony numer punktu był trudny” jest dobrym powodem, aby go nie używać. A jeśli chcesz mieć dobry powód, aby z nich korzystać, to determinizm, w szczególności determinizm na różnych platformach.
Zobacz też:
Dodatek : Sugeruję, aby świat był mały. Powiedziawszy to, oba OP i Jibb Smart poruszają kwestię, że odejście od pływaków początkowych ma mniejszą precyzję. Będzie to miało wpływ na fizykę, która będzie widoczna znacznie wcześniej niż krawędź świata. Stałe liczby punktowe, no cóż, mają ustaloną precyzję, będą wszędzie tak samo dobre (lub złe, jeśli wolisz). Co jest dobre, jeśli chcemy determinizmu. Chciałbym również wspomnieć, że sposób, w jaki zwykle uprawiamy fizykę, ma właściwość wzmacniania małych odmian. Zobacz efekt motyla - fizyka deterministyczna w The Incredible Machine and Contraption Maker .
Kolejny sposób uprawiania fizyki
Myślałem, że powodem, dla którego mały błąd w precyzji w liczbach zmiennoprzecinkowych jest wzmacniany, jest to, że wykonujemy iteracje na tych liczbach. Na każdym etapie symulacji bierzemy wyniki ostatniego etapu symulacji i robimy na nich różne rzeczy. Kumulacja błędów na szczycie błędów. To jest twój efekt motyla.
Nie sądzę, abyśmy zobaczyli, że jedna kompilacja korzystająca z jednego wątku na tej samej maszynie daje inną wydajność przy tym samym wejściu. Jednak na innej maszynie może to zrobić inna wersja.
Istnieje argument za przetestowaniem tam. Jeśli zdecydujemy dokładnie, jak powinny działać, i będziemy mogli przetestować na sprzęcie docelowym, nie powinniśmy wypuszczać kompilacji, które mają inne zachowanie.
Istnieje jednak również argument za niedziałaniem poza domem, który gromadzi tyle błędów. Być może jest to okazja do uprawiania fizyki w inny sposób.
Jak zapewne wiesz, istnieje ciągła i dyskretna fizyka, obie pracują nad tym, o ile każdy obiekt przesunąłby się w czasie. Jednak ciągła fizyka ma środki, aby dowiedzieć się o chwili zderzenia, zamiast sondować różne możliwe momenty, aby sprawdzić, czy doszło do zderzenia.
Proponuję zatem: użyj technik ciągłej fizyki, aby dowiedzieć się, kiedy nastąpi kolejne zderzenie każdego obiektu, z dużym krokiem czasowym, znacznie większym niż w przypadku jednego kroku symulacji. Następnie bierzesz najbliższą chwilę kolizji i zastanawiasz się, gdzie wszystko będzie w tej chwili.
Tak, to dużo pracy z jednego kroku symulacji. Oznacza to, że symulacja nie rozpocznie się natychmiast ...
... Możesz jednak przeprowadzić symulację kilku kolejnych kroków symulacji bez sprawdzania kolizji za każdym razem, ponieważ już wiesz, kiedy nastąpi następna kolizja (lub że kolizja nie nastąpi w dużym czasie). Co więcej, błędy skumulowane w tej symulacji są nieistotne, ponieważ gdy symulacja osiągnie duży czas, po prostu umieszczamy wcześniej obliczone pozycje.
Teraz możemy wykorzystać budżet czasu, który wykorzystalibyśmy do sprawdzania kolizji na każdym etapie symulacji, aby obliczyć kolejną kolizję po tej, którą znaleźliśmy. Oznacza to, że możemy symulować z wyprzedzeniem, korzystając z dużego czasu. Zakładając, że świat ma ograniczony zasięg (nie będzie to działać w przypadku dużych gier), powinna istnieć kolejka przyszłych stanów do symulacji, a następnie każda ramka, którą interpolujesz od ostatniego stanu do następnego.
Argumentowałbym za interpolacją. Biorąc jednak pod uwagę, że istnieją przyspieszenia, nie możemy po prostu interpolować wszystkiego w ten sam sposób. Zamiast tego musimy interpolować, biorąc pod uwagę przyspieszenie każdego obiektu. W tym przypadku moglibyśmy po prostu zaktualizować pozycję w ten sam sposób, co robimy dla dużego timepeptu (co oznacza również, że jest mniej podatny na błędy, ponieważ nie użylibyśmy dwóch różnych implementacji dla tego samego ruchu).
Uwaga : Jeśli wykonujemy te liczby zmiennoprzecinkowe, takie podejście nie rozwiązuje problemu obiektów zachowujących się inaczej, im dalej są od początku. Jednak prawdą jest, że precyzja jest tracona w miarę oddalania się od źródła, ale nadal jest deterministyczna. W rzeczywistości dlatego nawet nie wspomniał o tym pierwotnie.
Uzupełnienie
Od OP w komentarzu :
Więc nie ma formatu binarnego, prawda? Oznacza to, że musimy się również martwić, niezależnie od tego, czy odzyskane liczby zmiennoprzecinkowe odpowiadają oryginałowi. Zobacz: Ponownie odwiedzono Float Precision: Przenośność dziewięciocyfrowa
źródło
base64
elementów. Nie jest to skuteczny sposób reprezentowania dużych ilości takich danych, ale niewłaściwe jest sugerowanie, że zapobiegają one użyciu reprezentacji binarnych.Pracuję dla firmy, która tworzy pewną dobrze znaną grę strategiczną w czasie rzeczywistym i mogę powiedzieć, że determinizm zmiennoprzecinkowy jest możliwy.
Używanie różnych kompilatorów lub tego samego kompilatora z różnymi ustawieniami, a nawet różnych wersji tego samego kompilatora, może złamać determinizm.
Jeśli potrzebujesz gry krzyżowej między platformami lub wersjami gry, myślę, że musisz przejść do punktu stałego - jedyna możliwa gra krzyżowa, o której wiem, że jest zmiennoprzecinkowa, jest pomiędzy PC a XBox1, ale to dość szalone.
Musisz albo znaleźć silnik fizyki, który jest w pełni deterministyczny, albo wziąć silnik open source i uczynić go deterministycznym, albo zbudować własny silnik. Mam wrażenie, że Unity wszystkich rzeczy dodało deterministyczny silnik fizyki, ale nie jestem pewien, czy jest on po prostu deterministyczny na tej samej maszynie, czy deterministyczny na wszystkich maszynach.
Jeśli masz zamiar rzucić własne rzeczy, kilka rzeczy, które mogą pomóc:
źródło
rsqrtps
?Nie jestem pewien, czy tego typu odpowiedzi szukasz, ale alternatywą może być uruchomienie obliczeń na centralnym serwerze. Poproś klientów o przesłanie konfiguracji na twój serwer, pozwól jej przeprowadzić symulację (lub pobrać buforowaną) i odeślij wyniki, które następnie zostaną zinterpretowane przez klienta i przetworzone na grafikę.
Oczywiście wyłącza to wszelkie plany uruchomienia klienta w trybie offline, aw zależności od intensywności obliczeniowej symulacji może być potrzebny bardzo wydajny serwer. Lub wiele, ale przynajmniej masz możliwość upewnienia się, że mają taką samą konfigurację sprzętową i programową. Symulacja w czasie rzeczywistym może być trudna, ale nie niemożliwa (pomyśl o strumieniach wideo na żywo - działają, ale z niewielkim opóźnieniem).
źródło
Przedstawię kontr-intuicyjną sugestię, że chociaż nie jest w 100% niezawodna, powinna działać dobrze przez większość czasu i jest bardzo łatwa do wdrożenia.
Zmniejsz precyzję.
Użyj wcześniej ustalonego stałego rozmiaru kroku czasowego, wykonuj fizykę dla każdego kroku czasowego w standardowym zmiennoprzecinkowym podwójnej precyzji, ale następnie skwantyzuj rozdzielczość wszystkich zmiennych do pojedynczej precyzji (lub coś jeszcze gorszego) po każdym kroku. Wtedy większość możliwych odchyleń, które może wprowadzić zmiennoprzecinkowa zmiana (w porównaniu do przebiegu referencyjnego tego samego programu), zostanie usunięta, ponieważ odchylenia te występują w cyfrach, które nawet nie istnieją ze zmniejszoną precyzją. Zatem odchylenie nie ma szans na nagromadzenie Lapunowa (Efekt Motyla), które w końcu stanie się zauważalne.
Oczywiście symulacja będzie nieco mniej dokładna niż mogłaby być (w porównaniu z prawdziwą fizyką), ale nie jest to tak naprawdę godne uwagi, o ile wszystkie uruchomione programy są niedokładne w ten sam sposób .
Teraz, technicznie rzecz biorąc, jest oczywiście możliwe, że zmiana kolejności spowoduje odchylenie, które sięga do cyfry o wyższym znaczeniu, ale jeśli odchylenia są tak naprawdę spowodowane wyłącznie zmiennoprzecinkowym, a twoje wartości reprezentują ciągłe wielkości fizyczne, jest to niezwykle mało prawdopodobne. Zauważ, że
double
między dwoma dowolnymi jest pół miliarda wartościsingle
, więc można oczekiwać, że znaczna większość kroków czasowych w większości symulacji będzie dokładnie taka sama między przebiegami symulacji. Miejmy nadzieję, że kilka przypadków, w których odchylenie przejdzie przez kwantyzację, będzie w symulacjach, które nie trwają tak długo (przynajmniej nie z chaotyczną dynamiką).Radziłbym również, abyś rozważył całkowicie przeciwne podejście do tego, o co pytasz: zaakceptuj niepewność ! Jeśli zachowanie jest nieco niedeterministyczne, wówczas jest to faktycznie bliższe faktycznym eksperymentom fizyki. Dlaczego więc nie celowo losowo dobrać parametrów początkowych dla każdego przebiegu symulacji i wprowadzić wymóg, aby symulacja przebiegała konsekwentnie przez wiele prób? To nauczy o wiele więcej na temat fizyki i tego, jak zaprojektować maszyny, aby były wystarczająco solidne / liniowe, zamiast super delikatnych, które są realistyczne tylko w symulacji.
źródło
Stwórz własną klasę do przechowywania liczb!
Możesz wymusić deterministyczne zachowanie, jeśli dokładnie wiesz, jak zostaną wykonane obliczenia. Na przykład, jeśli jedynymi operacjami, z którymi masz do czynienia, są mnożenie, dzielenie, dodawanie i odejmowanie, wystarczy przedstawić wszystkie liczby jako liczbę wymierną. Aby to zrobić, wystarczy zwykła klasa Rational.
Ale jeśli chcesz poradzić sobie z bardziej skomplikowanymi obliczeniami (np. Funkcjami trygonometrycznymi), będziesz musiał sam napisać takie funkcje. Jeśli chcesz mieć sinus liczby, musisz być w stanie napisać funkcję zbliżoną do sinusa liczby, używając tylko operacji wymienionych powyżej. Wszystko to jest wykonalne i moim zdaniem omija wiele owłosionych szczegółów w innych odpowiedziach. Kompromis polega na tym, że zamiast tego będziesz musiał zająć się matematyką.
źródło
*
/
+
-
wtedy, mianowniki będą z czasem coraz większe.Jest tu trochę pomieszania terminologii. Układ fizyczny może być całkowicie deterministyczny, ale niemożliwy do modelowania przez przydatny okres czasu, ponieważ jego zachowanie jest niezwykle wrażliwe na warunki początkowe, a nieskończenie mała zmiana warunków początkowych spowoduje zupełnie inne zachowania.
Oto film z prawdziwego urządzenia, którego zachowanie jest celowo nieprzewidywalne, z wyjątkiem statystycznego:
https://www.youtube.com/watch?v=EvHiee7gs9Y
Łatwo jest zbudować proste systemy matematyczne (wykorzystujące tylko dodawanie i mnożenie), w których wynik po N krokach zależy od N-tego miejsca dziesiętnego warunków początkowych. Pisanie oprogramowania do spójnego modelowania takiego systemu na dowolnym sprzęcie komputerowym i oprogramowaniu, które użytkownik może mieć, jest prawie niemożliwe - nawet jeśli masz wystarczająco duży budżet na przetestowanie aplikacji na każdej prawdopodobnej kombinacji sprzętu i oprogramowania.
Najlepszym sposobem na rozwiązanie tego problemu jest zaatakowanie problemu u źródła: uczyń fizykę swojej gry tak deterministyczną, jak to konieczne, aby uzyskać powtarzalne wyniki.
Alternatywą jest próba uczynienia go deterministycznym poprzez ulepszenie oprogramowania komputerowego w celu modelowania czegoś, co nie jest zgodne z fizyką. Problem polega na tym, że wprowadziłeś jeszcze kilka warstw złożoności do systemu, w porównaniu z wyraźną zmianą fizyki.
Jako konkretny przykład, załóżmy, że twoja gra obejmuje zderzenia sztywnych ciał. Nawet jeśli zignorujesz tarcie, dokładne modelowanie kolizji między obiektami o dowolnym kształcie, które mogą się obracać podczas ruchu, jest w praktyce niemożliwe. Ale jeśli zmienisz sytuację tak, aby jedynymi obiektami były nieobrotowe prostokątne cegły, życie stanie się znacznie prostsze. Jeśli obiekty w grze nie wyglądają jak cegły, to ukryj ten fakt za pomocą „niefizycznej” grafiki - na przykład dosłownie ukryj chwilę kolizji za jakimś dymem lub płomieniami lub kreskówkową bańką tekstową „Ouch” lub cokolwiek.
Gracz musi odkryć fizykę gry, grając w nią. Nie ma znaczenia, czy nie jest ono „całkowicie realistyczne”, o ile jest spójne i wystarczająco podobne do zdrowego rozsądku, aby było prawdopodobne.
Jeśli sprawisz, że sama fizyka zachowuje się w stabilny sposób, jej model komputerowy może również dawać stabilne wyniki, przynajmniej w tym sensie, że błędy zaokrąglania będą nieistotne.
źródło
Użyj podwójnej precyzji zmiennoprzecinkowej , zamiast pojedynczej precyzji zmiennoprzecinkowej . Chociaż nie jest idealny, jest wystarczająco dokładny, aby uznać go za deterministyczny w twojej fizyce. Możesz wysłać rakietę na Księżyc z podwójną precyzją zmiennoprzecinkową, ale nie z pojedynczą precyzją zmiennoprzecinkową.
Jeśli naprawdę potrzebujesz idealnego determinizmu, użyj matematyki stałoprzecinkowej . To da ci mniejszą precyzję (zakładając, że używasz tej samej liczby bitów), ale wyniki deterministyczne. Nie znam żadnych silników fizyki, które używają matematyki stałoprzecinkowej, więc być może będziesz musiał napisać własny, jeśli chcesz iść tą drogą. (Odradzam.)
źródło
Użyj wzorca pamiątkowego .
W początkowej fazie zapisuj dane pozycji w każdej ramce lub w dowolnych testach porównawczych, których potrzebujesz. Jeśli jest to zbyt mało wydajne, rób to tylko co n klatek.
Następnie podczas odtwarzania symulacji postępuj zgodnie z dowolną fizyką, ale aktualizuj dane pozycyjne co n klatek.
Zbyt uproszczony pseudo-kod:
źródło