Wartości prędkości nie będące liczbami całkowitymi - czy jest na to czystszy sposób?

21

Często będę chciał użyć wartości prędkości, takiej jak 2,5, do poruszania moją postacią w grze opartej na pikselach. Wykrywanie kolizji będzie jednak ogólnie trudniejsze, jeśli to zrobię. W rezultacie robię coś takiego:

moveX(2);
if (ticks % 2 == 0) { // or if (moveTime % 2 == 0)
    moveX(1);
}

Kulę się w środku za każdym razem, gdy muszę to napisać, czy istnieje czystszy sposób na poruszenie postaci o wartościach liczb całkowitych innych niż całkowite, czy też utknę robienie tego na zawsze?

Akumulator
źródło
11
Czy zastanawiałeś się nad zmniejszeniem liczby całkowitej (np. 1/10 jednostki wyświetlacza), a następnie 2,5 oznacza 25, i nadal możesz traktować ją jako liczbę całkowitą dla wszystkich kontroli i konsekwentnie traktować każdą ramkę.
DMGregory
6
Możesz rozważyć przyjęcie algorytmu linii Bresenhama, który można zaimplementować przy użyciu tylko liczb całkowitych.
n0rd
1
Jest to powszechnie wykonywane na starych konsolach 8-bitowych. Zobacz artykuły takie jak ten, aby zobaczyć, jak ruch podpikseli jest realizowany za pomocą metod ustalonych punktów: tasvideos.org/GameResources/NES/BattleOfOlympus.html
Lucas

Odpowiedzi:

13

Bresenham

W dawnych czasach, kiedy ludzie nadal pisali własne podstawowe procedury wideo do rysowania linii i okręgów, nie było niespotykane stosowanie do tego algorytmu linii Bresenhama.

Bresenham rozwiązuje ten problem: chcesz narysować linię na ekranie, która przesuwa dxpiksele w kierunku poziomym, jednocześnie rozciągając dypiksele w kierunku pionowym. W liniach występuje nieodłączny „pływający” charakter; nawet jeśli masz piksele całkowite, skończysz na racjonalnych nachyleniach.

Algorytm musi być szybki, co oznacza, że ​​może używać wyłącznie arytmetyki liczb całkowitych; i unika się bez mnożenia lub dzielenia, tylko dodawanie i odejmowanie.

Możesz to dostosować do swojego przypadku:

  • Twój „kierunek x” (w sensie algorytmu Bresenhama) to Twój zegar.
  • Twój „kierunek y” to wartość, którą chcesz zwiększyć (tj. Pozycja twojej postaci - ostrożnie, to nie jest tak naprawdę „y” twojego duszka lub cokolwiek na ekranie, bardziej abstrakcyjna wartość)

„x / y” tutaj nie jest położeniem na ekranie, ale wartością jednego z twoich wymiarów w czasie. Oczywiście, jeśli twój duszek biegnie w dowolnym kierunku po ekranie, będziesz mieć wiele Bresenhamów biegnących osobno, 2 dla 2D, 3 dla 3D.

Przykład

Powiedzmy, że chcesz przenieść swoją postać prostym ruchem od 0 do 25 wzdłuż jednej ze swoich osi. Ponieważ porusza się z prędkością 2,5, będzie tam przy klatce 10.

Jest to to samo co „rysowanie linii” od (0,0) do (10,25). Chwyć algorytm linii Bresenhama i pozwól mu działać. Jeśli zrobisz to dobrze (a kiedy ją przestudiujesz, bardzo szybko stanie się jasne, jak to zrobić dobrze), to wygeneruje dla ciebie 11 „punktów” (0,0), (1,2), (2, 5), (3,7), (4,10) ... (10,25).

Wskazówki dotyczące adaptacji

Jeśli przejdziesz do tego algorytmu i znajdziesz jakiś kod (Wikipedia zawiera dość duży traktat), musisz pamiętać o kilku rzeczach:

  • Oczywiście działa na wszystkie rodzaje dxi dy. Jesteś zainteresowany jednym konkretnym przypadkiem (tzn. Nigdy go nie będziesz mieć dx=0).
  • Zwykła implementacja będzie miała kilka różnych przypadków dla ćwiartek na ekranie, w zależności od tego, czy dxi dysą pozytywne, negatywne, a także czy abs(dx)>abs(dy)nie. Tutaj oczywiście również wybierasz to, czego potrzebujesz. Musisz szczególnie upewnić się, że kierunek, który zwiększa się o 1każdy tik, jest zawsze twoim kierunkiem „zegarowym”.

Jeśli zastosujesz te uproszczenia, wynik będzie naprawdę bardzo prosty i całkowicie pozbędziesz się reali.

AnoE
źródło
1
To powinna być zaakceptowana odpowiedź. Po zaprogramowaniu gier na C64 w latach 80-tych i fraktali na PC w latach 90-tych, wciąż żałuję, że używam zmiennoprzecinkowego wszędzie tam, gdzie mogę tego uniknąć. Lub oczywiście, gdy FPU są wszechobecne w dzisiejszych procesorach, argument wydajności jest w większości dyskusyjny, ale arytmetyka zmiennoprzecinkowa wciąż potrzebuje znacznie więcej tranzystorów, pobierając więcej mocy, a wiele procesorów całkowicie wyłączy FPU, gdy nie są używane. Unikanie zmiennoprzecinkowe sprawi, że Twoi użytkownicy mobilni będą Ci wdzięczni za to, że nie wyczerpałeś tak szybko baterii.
Guntram Blohm obsługuje Monikę
@GuntramBlohm Zaakceptowana odpowiedź działa doskonale również w przypadku korzystania z Fixed Point, co moim zdaniem jest dobrym sposobem na zrobienie tego. Co sądzisz o liczbach stałych?
leetNightshade
Zmieniłem to na zaakceptowaną odpowiedź po tym, jak dowiedziałem się, jak to zrobili w dniach 8-bitowych i 16-bitowych.
Akumulator
26

Jest świetny sposób na robienie dokładnie tego, co chcesz.

Oprócz floatprędkości musisz mieć drugą floatzmienną, która będzie zawierać i kumulować różnicę między prędkością rzeczywistą a prędkością zaokrągloną . Różnica ta jest następnie łączona z samą prędkością.

#include <iostream>
#include <cmath>

int main()
{
    int pos = 10; 
    float vel = 0.3, vel_lag = 0;

    for (int i = 0; i < 20; i++)   
    {
        float real_vel = vel + vel_lag;
        int int_vel = std::lround(real_vel);
        vel_lag = real_vel - int_vel;

        std::cout << pos << ' ';
        pos += int_vel;
    }
}

Wydajność:

10 10 11 11 11 12 12 12 12 13 13 13 14 14 14 15 15 15 15 16

HolyBlackCat
źródło
5
Dlaczego wolisz to rozwiązanie od używania zmiennoprzecinkowego (lub stałego punktu) zarówno dla prędkości, jak i położenia, a zaokrąglanie pozycji do samych liczb całkowitych na samym końcu?
CodesInChaos
@CodesInChaos Nie wolę mojego rozwiązania od tego. Kiedy pisałem tę odpowiedź, nie wiedziałem o tym.
HolyBlackCat
16

Użyj wartości zmiennoprzecinkowych dla wartości ruchu i liczb całkowitych dla kolizji i renderowania.

Oto przykład:

class Character {
    float position;
public:
    void move(float delta) {
        this->position += delta;
    }
    int getPosition() const {
        return lround(this->position);
    }
};

Kiedy się poruszasz, używasz, move()który akumuluje pozycje ułamkowe. Ale kolizja i rendering mogą poradzić sobie z integralnymi pozycjami za pomocą getPosition()funkcji.

congusbongus
źródło
Należy pamiętać, że w przypadku gry w sieci używanie zmiennych zmiennoprzecinkowych do symulacji świata może być trudne. Zobacz np . Gafferongames.com/networking-for-game-programmers/… .
liori
@liori Jeśli masz klasę punktów stałych, która działa jak drop-in zamiennik dla float, czy nie rozwiązuje to w większości przypadków?
leetNightshade
@leetNightshade: zależy od implementacji.
liori
1
Powiedziałbym, że problem nie istnieje w praktyce, jaki sprzęt zdolny do uruchamiania nowoczesnych gier sieciowych nie ma pływaków IEEE 754 ???
Sopel,