Valeurs de vitesse non entières - existe-t-il un moyen plus propre de le faire?

21

Souvent, je veux utiliser une valeur de vitesse telle que 2,5 pour déplacer mon personnage dans un jeu basé sur des pixels. La détection des collisions sera généralement plus difficile si je le fais, cependant. Je finis donc par faire quelque chose comme ça:

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

Je grince des dents à l'intérieur chaque fois que je dois écrire cela, y a-t-il un moyen plus propre de déplacer un personnage avec des valeurs de vitesse non entières ou vais-je rester bloqué pour toujours?

Accumulateur
la source
11
Avez-vous envisagé de réduire la taille de votre unité entière (par exemple, 1 / 10ème d'une unité d'affichage), puis 2,5 se traduit par 25, et vous pouvez toujours la traiter comme un entier pour toutes les vérifications et traiter chaque trame de manière cohérente.
DMGregory
6
Vous pouvez envisager d'adopter l' algorithme de ligne de Bresenham qui peut être implémenté en utilisant uniquement des entiers.
n0rd
1
C'est une opération courante sur les anciennes consoles 8 bits. Voir des articles comme celui-ci pour un exemple de la façon dont le mouvement sous-pixel est mis en œuvre avec des méthodes à virgule fixe: tasvideos.org/GameResources/NES/BattleOfOlympus.html
Lucas

Réponses:

13

Bresenham

Autrefois, lorsque les gens écrivaient encore leurs propres routines vidéo de base pour dessiner des lignes et des cercles, il n'était pas rare d'utiliser l'algorithme de ligne de Bresenham pour cela.

Bresenham résout ce problème: vous voulez tracer une ligne sur l'écran qui déplace les dxpixels dans le sens horizontal tout en couvrant les dypixels dans le sens vertical. Il y a un caractère "flottant" inhérent aux lignes; même si vous avez des pixels entiers, vous vous retrouvez avec des inclinations rationnelles.

Cependant, l'algorithme doit être rapide, ce qui signifie qu'il ne peut utiliser que l'arithmétique entière; et il s'en sort aussi sans multiplication ni division, seulement addition et soustraction.

Vous pouvez l'adapter à votre cas:

  • Votre "direction x" (en termes de l'algorithme de Bresenham) est votre horloge.
  • Votre "direction y" est la valeur que vous souhaitez augmenter (c'est-à-dire la position de votre personnage - attention, ce n'est pas réellement le "y" de votre image-objet ou quoi que ce soit à l'écran, plus une valeur abstraite)

"x / y" ici ne sont pas l'emplacement sur l'écran, mais la valeur d'une de vos dimensions dans le temps. Évidemment, si votre sprite s'exécute dans une direction arbitraire à travers l'écran, vous aurez plusieurs Bresenhams fonctionnant séparément, 2 pour 2D, 3 pour 3D.

Exemple

Supposons que vous souhaitiez déplacer votre personnage d'un simple mouvement de 0 à 25 le long de l'un de vos axes. Comme il se déplace à la vitesse 2,5, il y arrivera à l'image 10.

Cela revient à "tracer une ligne" de (0,0) à (10,25). Prenez l'algorithme de ligne de Bresenham et laissez-le fonctionner. Si vous le faites correctement (et lorsque vous l’étudiez, il deviendra très rapidement clair comment vous le faites correctement), alors il générera 11 "points" pour vous (0,0), (1,2), (2, 5), (3,7), (4,10) ... (10,25).

Conseils sur l'adaptation

Si vous recherchez cet algorithme sur Google et que vous trouvez du code (Wikipédia a un traité assez large à ce sujet), vous devez faire attention à certaines choses:

  • Cela fonctionne évidemment pour toutes sortes de dxet dy. Cependant, vous êtes intéressé par un cas spécifique (c'est-à-dire que vous ne l'aurez jamais dx=0).
  • La mise en œuvre habituelle aura plusieurs cas différents pour les quarts de cercle sur l'écran, selon que dxet dysont positifs, négatifs, et aussi si abs(dx)>abs(dy)ou non. Vous choisissez bien sûr également ce dont vous avez besoin ici. Vous devez vous assurer particulièrement que la direction qui est augmentée à 1chaque tick est toujours la direction de votre "horloge".

Si vous appliquez ces simplifications, le résultat sera en effet très simple et éliminera complètement tous les réels.

AnoE
la source
1
Ce devrait être la réponse acceptée. Ayant programmé des jeux sur le C64 dans les années 80 et des fractales sur PC dans les années 90, je continue de grincer des dents en utilisant la virgule flottante partout où je peux l'éviter. Ou bien sûr, avec les FPU omniprésents dans les processeurs d'aujourd'hui, l'argument des performances est principalement théorique, mais l'arithmétique à virgule flottante a encore besoin de beaucoup plus de transistors, consommant plus d'énergie, et de nombreux processeurs fermeront complètement leurs FPU lorsqu'ils ne seront pas utilisés. Ainsi, en évitant la virgule flottante, vos utilisateurs mobiles vous remercieront de ne pas aspirer leurs batteries si rapidement.
Guntram Blohm prend en charge Monica
@GuntramBlohm La réponse acceptée fonctionne parfaitement bien quand vous utilisez également Fixed Point, ce qui, je pense, est un bon moyen de le faire. Que pensez-vous des nombres à virgule fixe?
leetNightshade
Modifié cela à la réponse acceptée après avoir découvert que c'est ainsi qu'ils l'ont fait dans les jours 8 bits et 16 bits.
Accumulateur
26

Il existe un excellent moyen de faire exactement ce que vous voulez.

En plus d'une floatvitesse, vous aurez besoin d'avoir une deuxième floatvariable qui contiendra et accumulera une différence entre la vitesse réelle et la vitesse arrondie . Cette différence est ensuite combinée avec la vitesse elle-même.

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

Sortie:

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

HolyBlackCat
la source
5
Pourquoi préférez-vous cette solution plutôt que d'utiliser float (ou virgule fixe) pour la vitesse et la position et d'arrondir la position à des entiers entiers à la fin?
CodesInChaos
@CodesInChaos Je ne préfère pas ma solution à celle-là. Lorsque j'écrivais cette réponse, je n'en savais rien.
HolyBlackCat
16

Utilisez des valeurs flottantes pour le mouvement et des valeurs entières pour la collision et le rendu.

Voici un exemple:

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

Lorsque vous vous déplacez, vous utilisez move()ce qui accumule les positions fractionnaires. Mais la collision et le rendu peuvent traiter des positions intégrales en utilisant la getPosition()fonction.

congusbongus
la source
Notez que dans le cas d'un jeu en réseau, l'utilisation de types à virgule flottante pour la simulation mondiale peut être délicate. Voir par exemple gafferongames.com/networking-for-game-programmers/… .
liori
@liori Si vous avez une classe à virgule fixe qui agit comme un remplacement direct pour float, cela ne résout-il pas principalement ces problèmes?
leetNightshade
@leetNightshade: dépend de l'implémentation.
liori
1
Je dirais que le problème est inexistant dans la pratique, quel matériel capable d'exécuter un jeu en réseau moderne n'a pas de flotteurs IEEE 754 ???
Sopel