Nicht ganzzahlige Geschwindigkeitswerte - gibt es eine sauberere Möglichkeit, dies zu tun?

21

Oft möchte ich einen Geschwindigkeitswert wie 2,5 verwenden, um meinen Charakter in einem pixelbasierten Spiel zu bewegen. Die Kollisionserkennung wird in der Regel schwieriger, wenn ich das mache. Also mache ich am Ende so etwas:

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

Ich schaudere jedes Mal, wenn ich das schreiben muss, nach innen. Gibt es eine sauberere Möglichkeit, ein Zeichen mit nicht ganzzahligen Geschwindigkeitswerten zu verschieben, oder bleibe ich dabei für immer stecken?

Akkumulator
quelle
11
Haben Sie darüber nachgedacht, Ihre Ganzzahleinheit zu verkleinern (z. B. 1/10 einer Anzeigeeinheit), dann wird 2,5 in 25 übersetzt, und Sie können sie weiterhin als Ganzzahl für alle Überprüfungen behandeln und jeden Frame konsistent behandeln.
DMGregory
6
Sie können den Bresenham-Linienalgorithmus verwenden, der nur mit ganzen Zahlen implementiert werden kann.
2.
1
Dies wird häufig auf alten 8-Bit-Konsolen durchgeführt. In Artikeln wie diesem finden Sie ein Beispiel dafür, wie Subpixelbewegungen mit Festkommamethoden implementiert werden: tasvideos.org/GameResources/NES/BattleOfOlympus.html
Lucas

Antworten:

13

Bresenham

In alten Zeiten, als die Leute noch ihre eigenen grundlegenden Videoroutinen zum Zeichnen von Linien und Kreisen schrieben, war es nicht ungewöhnlich, dafür den Bresenham-Linienalgorithmus zu verwenden.

Bresenham löst dieses Problem: Sie möchten eine Linie auf dem Bildschirm zeichnen, die dxPixel in horizontaler Richtung verschiebt und gleichzeitig dyPixel in vertikaler Richtung überspannt . Zeilen haben ein inhärentes "fließendes" Zeichen. Selbst wenn Sie ganzzahlige Pixel haben, haben Sie rationale Neigungen.

Der Algorithmus muss jedoch schnell sein, was bedeutet, dass er nur Ganzzahlarithmetik verwenden kann. und es kommt auch ohne Multiplikation oder Division davon, nur Addition und Subtraktion.

Sie können das für Ihren Fall anpassen:

  • Ihre "x-Richtung" (im Sinne des Bresenham-Algorithmus) ist Ihre Uhr.
  • Ihre "y-Richtung" ist der Wert, den Sie erhöhen möchten (dh die Position Ihres Charakters - Vorsicht, dies ist nicht das "y" Ihres Sprites oder was auch immer auf dem Bildschirm, eher ein abstrakter Wert).

"x / y" ist hier nicht die Position auf dem Bildschirm, sondern der Wert einer Ihrer Dimensionen in der Zeit. Wenn Ihr Sprite in einer beliebigen Richtung über den Bildschirm läuft, werden natürlich mehrere Bresenhams separat ausgeführt, 2 für 2D, 3 für 3D.

Beispiel

Angenommen, Sie möchten Ihren Charakter in einer einfachen Bewegung von 0 bis 25 entlang einer Ihrer Achsen bewegen. Da es sich mit Geschwindigkeit 2.5 bewegt, wird es dort bei Frame 10 ankommen.

Dies ist dasselbe wie "Zeichnen einer Linie" von (0,0) bis (10,25). Schnappen Sie sich Bresenhams Linienalgorithmus und lassen Sie ihn laufen. Wenn Sie es richtig machen (und wenn Sie es studieren, wird es sehr schnell klar, wie Sie es richtig machen), dann werden 11 "Punkte" für Sie generiert (0,0), (1,2), (2, 5), (3,7), (4,10) ... (10,25).

Hinweise zur Anpassung

Wenn Sie diesen Algorithmus googeln und Code finden (Wikipedia hat einen ziemlich großen Vertrag), gibt es einige Dinge, auf die Sie achten müssen:

  • Es funktioniert offensichtlich für alle Arten von dxund dy. Sie interessieren sich jedoch für einen bestimmten Fall (das heißt, Sie werden ihn niemals haben dx=0).
  • Die übliche Implementierung wird verschiedene Fälle für die Quadranten auf dem Bildschirm haben, abhängig davon, ob dxund ob dypositiv, negativ und auch ob abs(dx)>abs(dy)oder nicht. Natürlich wählen Sie hier auch aus, was Sie brauchen. Sie müssen besonders darauf achten, dass die Richtung, die mit 1jedem Tick erhöht wird, immer Ihre "Uhr" -Richtung ist.

Wenn Sie diese Vereinfachungen anwenden, wird das Ergebnis in der Tat sehr einfach sein und alle Realitäten vollständig beseitigen.

AnoE
quelle
1
Dies sollte die akzeptierte Antwort sein. Nachdem ich in den 80er Jahren Spiele auf dem C64 programmiert und in den 90er Jahren Fraktale auf dem PC erstellt habe, bin ich immer noch bestrebt, Gleitkommazahlen zu verwenden, wo immer ich sie vermeiden kann. Natürlich ist das Leistungsargument bei FPUs, die in heutigen Prozessoren allgegenwärtig sind, größtenteils umstritten, aber die Fließkomma-Arithmetik benötigt immer noch viel mehr Transistoren, die mehr Strom verbrauchen, und viele Prozessoren schalten ihre FPUs vollständig aus, wenn sie nicht verwendet werden. Wenn Sie Fließkommazahlen vermeiden, bedanken sich Ihre mobilen Benutzer dafür, dass Sie die Batterien nicht so schnell entladen haben.
Guntram Blohm unterstützt Monica
@GuntramBlohm Die akzeptierte Antwort funktioniert auch bei Verwendung von Fixed Point einwandfrei, was meiner Meinung nach eine gute Möglichkeit ist, dies zu tun. Wie stehen Sie zu Festkommazahlen?
LeetNightshade
Dies wurde in die akzeptierte Antwort geändert, nachdem festgestellt wurde, dass dies in den 8-Bit- und 16-Bit-Tagen der Fall war.
Accumulator
26

Es gibt eine großartige Möglichkeit, genau das zu tun, was Sie wollen.

Zusätzlich zu einer floatGeschwindigkeit benötigen Sie eine zweite floatVariable, die eine Differenz zwischen der reellen Geschwindigkeit und der gerundeten Geschwindigkeit enthält und akkumuliert . Diese Differenz wird dann mit der Geschwindigkeit selbst kombiniert.

#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;
    }
}

Ausgabe:

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

HolyBlackCat
quelle
5
Warum bevorzugen Sie diese Lösung gegenüber der Verwendung von float (oder Fixpoint) für Geschwindigkeit und Position und runden die Position am Ende nur auf ganze Zahlen?
CodesInChaos
@CodesInChaos Ich ziehe meine Lösung dieser nicht vor. Als ich diese Antwort schrieb, wusste ich nichts davon.
HolyBlackCat
16

Verwenden Sie Floating-Werte für die Bewegung und Integer-Werte für Kollision und Rendering.

Hier ist ein Beispiel:

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

Wenn Sie sich bewegen, verwenden Sie, move()die die Teilpositionen akkumuliert. Kollision und Rendering können jedoch mithilfe der getPosition()Funktion integrale Positionen verarbeiten .

congusbongus
quelle
Beachten Sie, dass die Verwendung von Gleitkommatypen für die Weltsimulation in einem Netzwerkspiel möglicherweise schwierig ist. Siehe zB gafferongames.com/networking-for-game-programmers/… .
Liori
@liori Wenn Sie eine Festkommaklasse haben, die als Ersatz für float fungiert, werden diese Probleme dann meistens behoben.
LeetNightshade
@leetNightshade: hängt von der Implementierung ab.
Liori
1
Ich würde sagen, das Problem ist in der Praxis nicht vorhanden. Welche Hardware, die in der Lage ist, ein modernes Netzwerkspiel auszuführen, hat keine IEEE 754-Floats?
Sopel