Animacja, wielowątkowość

0

Więc tak mam napisać grę i chcę wiedzieć czy koncepcja którą przyjąłem będzie dobra dla płynnej animacji i czy nie nastąpią problemy z interakcjami między wątkami.
Mam klasę:

public class MainGamePanel extends JPanel 
    implements Runnable, KeyListener, MouseListener ,MouseMotionListener
{
  private static final int PANEL_W = 1000; //wymiary panelu
  private static final int PANEL_H = 600;
  private Thread animator; // watek animujacy
  boolean running=false;
  public MainGamePanel()
  {
    addMouseListener(this);
    addKeyListener(this);
    setFocusable(true);
  }


  public void run()
 {
   while(running)
   {
     uptadeGame();
     renderGame();
     refreshImage();
     sleep(STALA-(czas_obecny-czas_poprzedni));
   }

  }
  @Override public void addNotify() //wystartuje po dodaniu panelu do Frame
  {
    super.addNotify();
    startGame();
  }

}

Ta klasa to jest tylko fragment podglądowy.
czas_obecny i czas_poprzedni będę uzyskiwał z jakiegoś timera i będzie to czas jaki upłyną od poprzedniego odświeżenia stanu gry, renderingu i odświeżenia obrazu.
STALA- to jakaś wartość w zależności od tego ile chcę FPS.
Ogólnie to będzie to bardziej rozbudowane, tak ze jeśli czas między kolejnymi wykonaniami pętli będzie za duży wątek nie zostanie uśpiony, chyba że przekroczona zostanie limit wykonań bez sleepa (np 16) to wtedy wykonany zostanie yield(). Myślę że to powinno działać dobrze, bo podobne rozwiązanie widziałem w tutorialu jakimś. Martwi mnie tylko jak animacja będzie współpracować z innymi wątkami które mają wpływ na wygląd obrazu.

Wymaganiem są jednostki z których każda musi być sterowana własnym, osobnym wątkiem.
Nie będę podawał szczegółów tylko chyba to co najważniejsze:

abstract public class Unit extends Thread
{
   public void run()
   {
     while(hp>0) //hit points jednostki
     {
       Unit enemy=EnemyInRange();//sprawdz czy wrog w zasiegu
       if (enemy!=null) attack(enemy); //atakuj jesli wrog w zasiegu
       if (moving) move(); //jesli ma rozkaz poruszania to sie porusza w wybrane miejsce
       try{
         sleep(20);
       }
       catch(InterruptedException e){};
     }
     owner.units.removeElement(this);

   };

}

To też tylko fragment podglądowy i klasa będzie bardziej rozbudowana.

Czy w przypadku klasy Unit wywołując sleepa muszę postąpić tak jak w klasie MainGamePanel, czyli z timera liczyć czas jaki upłyną i sleepa na odpowiedni czas ustawić? Bo jak zostawię stałą wartość 20 to te jednostki mogą się chyba z różną prędkością na komputerach o różnej wydajności poruszać?
Czy dałoby się zrobić coś takiego, i czy byłoby to dobre rozwiązanie gdyby w MainGamePanel.uptadeGame() wywołać notifyAll() a w klasie Unit w tej pętli w run() dać wait(). Wtedy wszystkie Unit'sy wykonałyby swój jeden cykl pętli dokładnie raz w ciągu cyklu pętli animującej.

Teraz to działa tak że EnemyInRange() przechodzi vector jednostek wroga i sprawdza czy odległość jest mniejsza od swojego zasięgu ataku. Czy ta funkcja powinna być synchronized ze względu na współrzędne wroga czy nie? Tzn. czy wewnątrz tej funkcji założyć lock na współrzędne wrogiej jednostki żeby ona w tym czasie ich przypadkiem nie zmieniła?

Jak najlepiej zrobić wyświetlanie tych jednostek? Planuję zrobić tak żeby zrobić funkcję Unit.draw(graphics g) która rysuje gifa na współrzędnych na których znajduje się jednostka.
W MainGamePanel.render() planuje zrobić wrzucenie tła do bufora a później po kolei wywołać Unit.draw(g) i rysować na tym buforze jednostki. Czy funkcja Unit.draw(graphics g) też powinna zakładać lock na współrzędne jednostki, żeby przypadkiem się nie zmieniły?

Później MainGamePanel.refreshImage() wrzuca bufor na ekran i kończy się przetwarzanie tej klatki.

Czy takie rozwiązanie będzie dobrze działać? Czy powinienem to jakoś inaczej zrobić.

0

To nie będzie dobrze działać w ogólnym przypadku.
Po pierwsze popatrz na główną pętlę swojej gry. Będzie ona musiała działać tak samo na komputerach z wolnym procesorem (jednym/wieloma) i/lub grafiką jak na tych szybkich. Czyli z różną prędkością będą wykonywać się zarówno refreshImage (grafika) jak i cała pętla ze wszystkimi wywołaniami w niej.
Jest w grach jeden niezmiennik, którego musisz się trzymać. Otóż tym niezmiennikiem jest liczba aktualizacji gry w ciągu sekundy. W Twoim przypadku chodzi o UpdateGame(). Ta metoda musi wywoływać się na KAŻDYM komputerze identyczną ilość razy w ciągu tego samego okresu czasu (np. sekundy). Jest to nazywane skrótem UPS (updates per second). Jeżeli gra jest mało reaktywna jak np. pacman, to wystarczy 10-20 aktualizacji na sekundę, natomiast we współczesnych grach UPS zwykle wynosi od 80 do 100 (szczególnie dotyczy to strzelanek).
Dlaczego tak jest? Pokażę to na przykładzie połączonych sieciowo gier. Nie może być sytuacji w której na jednym komputerze jakaś jednostka 10 razy zauważy, że coś się w otaczającym ją świecie zmieniło i w reakcji na te zmiany wykrzyczy na ekranie np. "WTF?", podczas gdy na drugim komputerze sąsiadująca z nią jednostka zauważy w tym samym okresie czasu tylko 5 zmian świata,a w związku z tym wykrzyczy "WTF?" zaledwie 5 razy. :)
Podobnie ze sterowaniem przez człowieka postacią z giwerą - w każdym przypadku jeżeli postać musi się obrócić w ciągu sekundy i strzelić, to na różnych komputerach gracz musi mieć tyle samo odcinków czasu na wykonanie tej samej akcji. Bez względu na to czy na ekran idzie mu 100 obrazów na sekundę czy tylko 20. To ostatnie to właśnie FPS (frames pr second) i jest całkowicie uzależnione od prędkości komputera.
Niektóre starsze gry nie utrzymywały niezmiennika UPS - te gry na współczesnych komputerach są niegrywalne (zbyt szybkie). Z nowszych gier błąd taki występował w Neverwinter Nights 2 do wersji 1.02 (np. polska premierowa).

Jak z tego widzisz musisz ulepszyć swoją pętlę tak aby UpdateGame było wykonywane te samą liczbę razy w ciągu sekundy na starym gracie i na wielordzeniowej nówce. Musi więc to być metoda ekstremalnie szybka czyli musi skupiać się najlepiej wyłącznie na aktualizacji świata gry oraz stanu kontrolerów tej gry - co jest najważniejszym powodem tej niezmienności.
Aby to osiągnąć Twój kod musi obliczać jak dużo czasu zabiera wykonanie renderGame() oraz refreshImage() i jeżeli ten czas byłby zbyt długi, to powinna redukować liczbę wykonań obu tych metod, a jeżeli byłby zbyt krótki, to powinna wprowadzać uśpienie (sleep) na taki czas aby kolejny przebieg pętli rozpoczął się w następnym odcinku czasu. Przy takiej konstrukcji pętli mogłaby ona wykonywać odkładnie np. 100 iteracji w ciągu sekundy, a jednocześnie tylko 30 renderGame i refreshImage. Musisz też uwzględnić, że zależnie od tego czy wąskim gardłem jest grafika czy procesor największym spowalniaczem będzie albo renderGame, albo refreshImage. Zazwyczaj będzie nim renderGame ponieważ tam będzie do wykonania najwięcej obliczeń. RefreshImage, z kolei operuje wielkimi ilościami danych, a w przypadku braku akceleracji i dużej rozdzielczości może okazać się ogromnym spowalniaczem.
Poza tym w tej samej pętli musisz umieścić też odczyty czasu - i to na pewno nie w formacie daty, ale raczej z użyciem metody System.nanoTime().
Ponieważ liczba wywołań UpdateGame będzie stała w czasie, to będzie to też bardzo dobry miernik czasu dla dużych odcinków. Np. jeżeli UPS będzie równe 60, to wyrenderowanie stanu licznika wykonania tej metody na ekranie powinno dać Ci stale korygujący się zegar sekundowy, który będzie zwiększał się co sekundę niezależnie od tego czy wykonane zostanie 60 czy 15 renderów i aktualizacji ekranu w ciągu każdej sekundy.

Nie jest też powiedziane, że musi się wykonać tyle samo renderów co aktualizacji ekranu. Nie ma większego sensu aby liczba aktualizacji ekranu była większa niż liczba renderów, ale w odwrotną stronę może to być prawdziwe.
W każdym razie zrobienie korekty czasowej w takiej pętli, to spora zabawa z obliczeniami. Na dodatek muszą one być ekstremalnie szybkie - bez żadnych dzieleń i mnożeń ponieważ będzie to najczęściej wykonywany kod w całej grze. Im mniejsze okażą się odchyłki tych odcinków czasu od zegarowego ideału tym lepiej. Górną granicą tej stabilności jest rozdzielczość zegara na podstawie którego liczone są odcinki czasu. Na przykład jeżeli zegar liczy z dokładnością do 30 ns, to minimalna odchyłka powinna wynosić do 60 ns.

Co do AI jednostek w grze, to również one muszą mieć swój (ale podobny) mechanizm wykonywania swoich iteracji stałą liczbę razy w odcinku czasu. Różnica polegać będzie tylko na tym, że nie będzie tam renderowania, ani aktualizacji obrazu. Alternatywnie można umieścić obsługę AI w kodzie aktualizującym grę, ale wtedy AI ta będzie działała jednowątkowo, no i utrzymanie stałego UPS przy sporo dłuższym czasie wykonania UpdateGame będzie trudniejsze bo będzie pewna minimalna wydajność CPU dla której gra będzie w ogóle mogła działać.

Co do pytania o wait i notifyAll, to raczej unikaj ich używania. Są to metody najniższego poziomu do synchronizacji wielowątkowości na zaawansowanym poziomie. Korzysta z nich biblioteka standardowa Javy w tym różne Workery, kolejki i kolekcje synchronizowane. Ty pisząc grę będziesz mógł z nich korzystać nie zagłębiając się w szczegóły implementacji na tak niskim poziomie. Na przykład wszelkie zmiany świata wykonane przez jednostki AI będzie można wrzucać do kolejki synchronizowanej, a na jej wyjściu wątek aktualizujący stan gry (czyli gameUpdate) będzie sobie te dane odbierał i aktualizował bazę ze stanem świata gry. Możliwości jest mnóstwo. Obecnie jednak jest lepiej założyć, że będzie dostępne realnie 8 lub 16 współbieżnych wątków dzięki czemu gra będzie działała znacznie wydajniej na przyszłych komputerach z 8 lub 16 rdzeniami.

0

Dzięki za pomoc. Te klasy były tylko podglądowe i od samego początku zamierzałem wprowadzić liczenie czasu tak jak zasugerowałeś o czym nawet napisałem :P

Ta klasa to jest tylko fragment podglądowy.
czas_obecny i czas_poprzedni będę uzyskiwał z jakiegoś timera i będzie to czas jaki upłyną od poprzedniego odświeżenia stanu gry, renderingu i odświeżenia obrazu.
STALA- to jakaś wartość w zależności od tego ile chcę FPS.

Dokładniej to u mnie wygląda to teraz tak:

    long previousTime,currentTime,timeDiff,sleepTime;
    int sleepless=0;//ilosc klatek bez sleepa
    previousTime=System.currentTimeMillis();
    while(running)
    {
        updateGame();
        renderGame();
        refreshImage();
        currentTime=System.currentTimeMillis();
        timeDiff=currentTime-previousTime;
        sleepTime=gameHandle.getInterval()-timeDiff;
        
        if (sleepTime>0) //mozna wywolac sleepa, watek zmiescil sie w czasie
        {
            try
            {
              Thread.sleep(sleepTime);
            }
        catch (InterruptedException e){}

        }
        else
          if(++sleepless>=SLEEPLESS_FRAMES) //za duzo klatek bez sleepa
          {
            Thread.yield();
            sleepless=0;
          }
        previousTime=System.currentTimeMillis();

    }//end while

updateGame() nic na razie nie robi i nie wiem co może robić bo jednostki same się aktualizują, a później w renderGame rysuję przebiegając po wektorze tych jednostek i rysując odpowiadający im obrazek na współrzędnych jednostki. Tutaj mam pytanie czy to powinno być synchronized czy nie? Bo renderGame potrzebuje tylko odczytać Rectangle odpowiadający jednostce, a tylko jednostka właściciel może tego Rectangle modyfikować.
Czy dobrze myślę że nawet jeśli renderGame() pobiera tego Rectangle a w tym czasie jednostka go modyfikuje to najwyżej gif zostanie narysowany na starych współrzędnych, ale to i tak będzie niezauważalne gdyż przy następnym renderze otrzyma już poprawne współrzędne te które są obecnie. Trochę może to opisałem w dziwny sposób, ale może ktoś zrozumie o co mi chodzi :D
Jednym zdaniem: potrzebne jest przy rysowaniu synchronized czy raczej można to olać?

Jednostki też mają odpowiedniego sleepa.

while(hp>0)
     {
       if(GameHandler.get().isPaused()) 
       { try{
         sleep(50);
         }catch(InterruptedException e){}; 
         continue;
       }
       Unit enemy=EnemyInRange();//sprawdz czy wrog w zasiegu
       if (enemy!=null) attack(enemy); //atakuj jesli wrog w zasiegu
       if (moving) move();
       //update();
       try{
         sleep(myTimer.getSleepTime());
       }
       catch(InterruptedException e){};
       myTimer.setPreviousTime();
     }

myTimer działa podobnie jak kod w Panelu. Ma jakaś zadaną stała ile odświeżeń na sekundę ma wykonywać i oblicza czas na podstawie długości trwania rendera itp. Dla 50 fps czyli 20ms na odświeżenie za każdym razem zwraca mi 20, ale chyba działa to poprawnie, bo jeśli w środku pętli wrzucę sleep na x milisekund to getSleepTime zwróci 20 pomniejszone o tą liczbę, więc wygląda że liczy poprawnie.

Ty stosujesz System.nanoTime() - czy System.currentTimeMillis() jest za mało dokładny? Czy różnice będą zauważalne w zastosowaniu nanoTima a currentTimeMillis?

I pojawia się problem jak zrobić pauze gry. Na razie mam tak: w Panelu gdy jest pauza gameUpdate() nic nie robi (u mnie i tak nic nie robi :D), odświeżanie i renderowanie jest włączone bo nie mam paintComponenta, używam active renderingu. A jak widać jednostka sprawdza czy jest pauza, jeśli jest to sleep(50) i continue. Czy da się w inny ładniejszy sposób rozwiązać? Tak żeby wątek się nie wykonywał wcale aż pauza zostanie wyłączona.

0

updateGame() nic na razie nie robi i nie wiem co może robić bo jednostki same się aktualizują

Przede wszystkim metoda ta służy do odczytu kontrolerów i zmiany stanu gry. Stan gry, to właściwie cała baza danych na podstawie których renderuje się grę. Tak więc absolutne minimum, to odczyt kontrolerów (pada, myszy, klawiszy itp.) i reakcja na nie w świecie gry.

Tutaj mam pytanie czy to powinno być synchronized czy nie?

Zawsze. I to bez względu na to czy następuje zapis, czy tylko odczyt. Jeżeli operacje zapisu i odczytu nie są atomowe, to brak synchronizacji skutkuje odczytem śmieci, np. połowicznie zapisanych słów pamięci, albo zapisem śmieci (np. przy dwóch jednoczesnych zapisach). A takie coś, to awaria danych i wykrycie jej powinno kończyć się nienaprawialnym wyjątkiem.
Jest możliwa zamiana synchronizacji na atomowe operacje odczytu i zapisu, ale dotyczy to tylko pojedynczych i prostych danych.

Co do nowej postaci głównej pętli, to nadal tyle razy wykona się updateGame ile renderGame i RefreshImage. Musisz liczyć czasy zarówno pierwszej metody, drugiej jak i trzeciej i na bieżąco korygować decyzję czy wystarczy czasu na RefreshImage lub renderGame i RefreshImage tak aby updateGame z kolejnej iteracji rozpoczęło się w dokładnie przewidzianej ramce czasu. Jak już napisałem to jedyny niezmiennik, którego gra musi się ściśle trzymać. Można olać wszystko inne, ale aktualizacje stanu kontrolerów muszą się wykonywać zawsze w konkretnych odcinkach czasu.

Co do modyfikacji świata i stanu gry przez poszczególne wątki - muszą one być synchronizowane na danych które odczytują i/lub modyfikują - ale pojawia się problem konieczności rozproszenia tych danych tak aby wszystkie wątki nie dobijały się o jakąś tę samo często wykorzystywaną zmienną bo bardzo łatwo dojdzie do deadlocka lub zagłodzenia wątków. Musisz też mieć dobry mechanizm na to aby AI poszczególnych jednostek nie konkurowały ze sobą o czas procesora bo inaczej dojdzie do sytuacji w których jedne jednostki nie będą wcale reagować "bo mają pecha", a inne będą reagować o wiele za często ("bo mają szczęście"). Dlatego najlepiej jest aby AI jednostek, które są w zasięgu swoich reakcji lub wspólnych zasobów gry było realizowane przez jeden wspólny wątek, a aktualizacja bazy była pojedynczą operacją dla całej takiej grupy. Synchronizowaną oczywiście.

myTimer działa podobnie jak kod w Panelu. Ma jakaś zadaną stała ile odświeżeń na sekundę ma wykonywać i oblicza czas na podstawie długości trwania rendera itp.

Nigdy niczego nie uzależniaj od czasu trwania rendera. Render jest zmienny i może go nawet w ogóle nie być w bieżącym okienku czasowym (nie starczy czasu). Jedyną stałą od której możesz coś uzależniać jest moment rozpoczęcia updateGame.
Chcesz się przekonać, że tak jest, to dodaj sobie jakieś sleep (może być losowy) do rendera i refreshImage (zasymuluje to powolny komputer), a sam się przekonasz, że program pójdzie w maliny.

I pojawia się problem jak zrobić pauze gry.

Na głównej pętli jako zmienną boolean anulującą wykonanie updateGame (czyli modyfikacji świata gry). Mimo pauzy gra nadal powinna być renderowana i odrysowywana. Na przykład w wielu grach podczas pauzy możesz obracać kamerą, grzebać w menu itp. Co do innych wątków, to wykrycie przez nie pauzy mogłoby wywoływać u nich metodę wait(), a jej przerwanie wygenerowanie dla tych wątków notifyAll(), aby kontynuować ich działanie.
Tu masz tutorial jak to wykorzystać:
http://www.java-samples.com/showtutorial.php?tutorialid=306

0

myTimer działa podobnie jak kod w Panelu. Ma jakaś zadaną stała ile odświeżeń na sekundę ma wykonywać i oblicza czas na podstawie długości trwania rendera itp.

Nigdy niczego nie uzależniaj od czasu trwania rendera. Render jest zmienny i może go nawet w ogóle nie być w bieżącym okienku czasowym (nie starczy czasu). Jedyną stałą od której możesz coś uzależniać jest moment rozpoczęcia updateGame.
Chcesz się przekonać, że tak jest, to dodaj sobie jakieś sleep (może być losowy) do rendera i refreshImage (zasymuluje to powolny komputer), a sam się przekonasz, że program pójdzie w maliny.

Dla 50 fps czyli 20ms na odświeżenie za każdym razem zwraca mi 20, ale chyba działa to poprawnie, bo jeśli w środku pętli wrzucę sleep na x milisekund to getSleepTime zwróci 20 pomniejszone o tą liczbę, więc wygląda że liczy poprawnie.

Napisałem że po dodaniu sleepa w środku tej pętli działa wszystko ok. Chyba się nie zrozumieliśmy. Sleep musi być zależny od rendera i od długości wykonywania się instrukcji w pętli. Jeśli mam 50 FPS to te instrukcje muszą zajmować 20ms. Czyli jeśli render, update, itp zajmie 15ms to ustawiam sleep na 5, jeśli render trwał krócej to sleep będzie dłuższy. A co do Unit to faktycznie trochę źle to ująłem, bo sleep dla jednostki nie jest zależny od rendera tylko też od czasu wykonywania się instrukcji w pętli while w metodzie run().

Teraz pojawił się u mnie dziwny problem, pewnie coś z synchronizacją wątków, mam klasę


public class PlayerUnit extends Unit
{
  //--------flagi do okreslania jak jednostka powinna sie poruszac
  private int moveFlag;
  private int tankRotation;
  private Point cursorPosition;
  private boolean firing;

  synchronized public void setMove(int flag)
  {
     moveFlag=flag;
      System.out.println("setting flag: ");
      System.out.println(moveFlag);
  }
  synchronized public void stopMoving(){moveFlag=NONE;System.out.println("stoping");}
  synchronized public void setRotation(int flag){tankRotation=flag;}
  synchronized public void stopRotation() {tankRotation=NONE;}
  synchronized public void refreshCursorPosition(int x,int y) {cursorPosition.x=x;cursorPosition.y=y;}
  synchronized public void startFire(){firing=true;}
 synchronized public void holdFire(){firing=false;}
    @Override
  protected void move()
  {

    Point tmp=new Point();
    //obliczenie kierunku wektora ruchu,
    //actualAngle- kat odchylenia od wektora 0,-1 -kierunek polnocny
    synchronized (this){
    tmp.x=-Math.sin(actualAngle);
    tmp.y=Math.cos(actualAngle);
    System.out.println(moveFlag);
    pos.x+=moveFlag*tmp.x;
    pos.y+=moveFlag*tmp.y;
   //System.out.print(pos.x);System.out.print(" ");
     //System.out.println(pos.y);
    }

  }

  public void run()
   {
     init();
      myTimer.init();
     while(hp>0)
     {
       if(GameHandler.get().isPaused())
       { try{
         sleep(50);
         }catch(InterruptedException e){};
         continue;
       }
       move();
       try{
         sleep(myTimer.getSleepTime());
       }
       catch(InterruptedException e){};
       myTimer.setPreviousTime();
     }
   

   };
    @Override
  synchronized public void init()
  {
     cursorPosition=new Point();
     moveFlag=2;
  }
  PlayerUnit()
  {
    super();
    cursorPosition=new Point();
  }
}

Chodzi o moveFlag. Jak widać jest prywatna więc może być zmieniona tylko przez metody set. Metoda setMove wywołuje się przy wciśnięciu klawisza i na konsole wyświetla się dobra wartość moveFlag.
Metoda stop też działa poprawnie i po zwolnieniu klawisza flaga jest poprawnie ustawiana, przynajmniej przy wypisaniu. Ale gdy wypiszę moveFlag w metodzie move() to jest ona stała i wynosi tyle ile nadano jej w metodzie init. Nie wiem o co chodzi, coś na pewno jest źle z synchronizacją wątków ale nie wiem w którym miejscu. Wygląda to tak że w metodzie setMove flaga jest ustawiona poprawnie, jednak po opuszczeniu jej flaga powraca do stanu jaki miała przy inicjalizacji.

0

Problem rozwiązany. Okazało się że w Panelu jak pobierałem gracza, obiekt nie został chyba utworzony i zainicjalizowany do końca i w Panelu znajdowała się niepoprawna referencja.
Zmieniłem teraz żeby za każdym wywołaniem tej funkcji set pobrany został obiekt gracza i wszystko już działa.

0

@Olamagato
Na poczatek wielkie dzieki za posty w tym watku. Co prawda nie jestem autorem pytan, ale zbieralem sie do utworzeia podobnego tematu i Twoje posty na prawde duzo wyjasnily.

Jesli mozna sie podpiac, mialbym kilka pytan w tej kwestii.

Takze przymierzam sie do stworzenia gry "real time", wiec nieco juz rozmyslalem o problemach szybkosci gry na roznych maszynach i doszedlem do nieco innego pomyslu w kwestii updateGame():
Planowalem w kazdej kolejnej iteracji petli liczyc ile czasu minelo od poprzedniej (calej petli - lacznie z potencjalnymi render i refresh) i wykorzystywac to jako swego rodzaju mnoznik akcji wewnatrz updateGame(). Jesli np ruch sprite'a o 1px ma trwac 100 ms, a ostatnia iteracja trwala 70ms, w updateGame() jednostka wykona ruch o 0.7px.
Wada tego rozwiazania jest to, ze wiekszosc rzeczy musialbym liczyc na liczbach zmiennoprzecinkowych (powyzsze przesuniecie o 0.7px na liczbach calkowitych nie spowodowaloby zadnego ruchu) i rzutowac do liczb calkowitych przy kazdym renderowaniu, a zaprezentowany przez Ciebie sposob ze stala czestotliwoscia aktualizacji swiata gry wydaje sie ten klopot usuwac.
Przy 100 UPS wystarczy przesuwac jednoste o 1px co 10 UPS.

Tylko co w sytuacji, gdy chwilowe spowolnienie procesu (duzo wiecej obliczen, czy inna aplikacja zabierajaca moc obliczeniowa) spowoduje, ze petla nawet mimo braku renderowania i refresh'a zacznie miec zaleglosci? Nie przejmowac sie tym i po prostu skonstruowac tak, zeby wykonywala tylko updateGame(), dopoki nie nadrobi zaleglosci? W moim przypadku wlasnie to bylo "na plus", gdyz kolejny updateGame() ze sporym mnoznikiem nadrobilby zaleglosc w jednym przebiegu.

I drugie pytanie. W proponowanym przez Ciebie algorytmie render i refresh wykona sie nie wiecej razy niz zdefiniowany UPS. Dziala to jako swego rodzaju frame limiter, ale jednak sporo gier czegos takiego nie ma i potrafia wyciagnac wiecej fps, czyli stosuja jeszcze inny schemat?

0
VGT napisał(a)

Tylko co w sytuacji, gdy chwilowe spowolnienie procesu (duzo wiecej obliczen, czy inna aplikacja zabierajaca moc obliczeniowa) spowoduje, ze petla nawet mimo braku renderowania i refresh'a zacznie miec zaleglosci? Nie przejmowac sie tym i po prostu skonstruowac tak, zeby wykonywala tylko updateGame(), dopoki nie nadrobi zaleglosci?

Dokładnie. Jeżeli komputer jest zbyt wolny (stale lub czasem), to nic tego nie nadrobi w przypadku renderingu real-time. Zadaniem części graficznej jest odrysowywanie aktualnego świata gry, a nie zabawa w rozciąganie lub ściskanie czasu. :). Jeżeli przez jakiś moment nie można tego stanu świata przekazać, to jedyną możliwością jest ominięcie przekazu. I nie ma tu mowy o żadnych zaległościach - po prostu gra przekazuje stan bieżący, a z jakim ziarnem czasowym to robią, to sprawa wtórna.
W niektórych grach (ale nie tylko bo tego samego schematu używają wszelkie rendery obrazu telewizyjnego i animatory) jeżeli opóźnienie jest zbyt długie w pojedynczym kawałku, to bywa że istnieje w kodzie wyjątek na żądanie renderowania minimalnej liczby klatek. Na przykład takim prymitywnym założeniem może być obowiązkowe wyrenderowanie np. co 20. klatki. Działa to gdy pełne zaktualizowanie gry, wyrenderowanie i wyrzucenie na ekran klatki zajmuje więcej niż sekundę. Bez takiego zabezpieczenia gra nigdy nie wyrzuci ani jednej klatki czyli ekran pozostałby zawsze "doskonale czarny". ;)
Przy innym podejściu i zabezpieczeniu obowiązkowego renderowania jednej klatki na sekundę cały obrót pętli będzie trwał więcej niż sekundę. Wtedy cała gra będzie również spowalniać swoje działanie, a nie tylko zrzucać klatki. Oczywiście nie uda się zachować stałego okna czasowego dla updateGame. Taki objaw można też zaobserwować w praktycznie każdej komercyjnej grze odpalonej na starym rzęchu lub z użyciem programu stress.

Żeby nikogo nie zmyliło, lagi w grach mają inną przyczynę. Jest nią zrywanie lub opóźniona transmisja sieciowa, dzięki czemu nie można prawidłowo lub w ogóle aktualizować stanu świata gry.

I drugie pytanie. W proponowanym przez Ciebie algorytmie render i refresh wykona sie nie wiecej razy niz zdefiniowany UPS. Działa to jako swego rodzaju frame limiter, ale jednak sporo gier czegos takiego nie ma i potrafia wyciagnac wiecej fps, czyli stosuja jeszcze inny schemat?

Zgadza się, że przy takim podejściu liczba UPS jest maksymalną możliwą do wygenerowania liczbą FPS. Są dwa rozwiązania tego sposobu ale tylko jeden jest stosowany w praktyce. Pierwszy, to ustalenie sztywnej dużej liczby UPS - na przykład na poziomie 200 UPS, co da maksymalną liczbę FPS też na poziomie 200. Ten sposób jest niemal zawsze stosowany (szczególnie w grach akcji), w których szybka reakcja gry jest kluczowa, jest też to standardowym sposobem rozwiązania problemu synchronizacji w sieci. Nie ma w praktyce górnego limitu UPS dopóki czas wykonania wszystkich założonych aktualizacji gry nie przekroczy sekundy. Tyle, że wtedy w takim przypadku granicznym gra będzie zrzucać 99% klatek. Dlatego najlepiej oszacować wielkość UPS na takim poziomie aby suma czasów żądanych aktualizacji nie przekraczała 100-250 milisekund (10-25% okienka czasowego). Na tej podstawie można też dość łatwo określić minimalne wymagania wydajnościowe komputera (a raczej całego środowiska). Trzeba pamiętać, że człowiek i tak świadomie nie zobaczy więcej niż 30 klatek/s oraz wtórnego efektu stroboskopowego powyżej 85 klatek/s.

Drugim sposobem jest wykonanie pewnej ilości "ślepych" renderów lub innego wewnętrznego benchmarka, aby ewentualnie podnieść liczbę UPS, i tym samym maksymalną liczbę FPS. Jednak ten sposób jest chyba na wymarciu bo dwa ważne powody przemawiają przeciwko niemu: Pierwszy, to brak możliwości synchronizowania gier w sieci, a drugi, to brak sensu w marnowaniu mocy obliczeniowej (i tym samym prądu) w komputerze, który wielokrotnie przekracza potrzeby wydajnościowe gry. Na przykład taki Starcraft nadal robi te 30-60 klatek na sekundę mimo, że grając w tę grę na współczesnym komputerze, procesor większość czasu każdej mijającej sekundy przeleży w trybie halt (czyli idle). Wersja niespatchowana (np. 1.0), która nie posiada takiego mechanizmu będzie natomiast produkować 100-500 FPS i o tyleż szybciej będzie działać. Oczywiście w takim wypadku gra nie będzie grywalna na dzisiejszych komputerach. Gry takie oprócz zbyt szybkiego działania zawsze powodują raportowanie 100% czasu procesora. Oczywiście pomijając wywłaszczanie czyli czas, który "wyrywa" sobie system operacyjny dla siebie i innych aplikacji.

1 użytkowników online, w tym zalogowanych: 0, gości: 1