Valores de velocidad no enteros: ¿hay una forma más limpia de hacer esto?

21

A menudo querré usar un valor de velocidad como 2.5 para mover a mi personaje en un juego basado en píxeles. Sin embargo, la detección de colisión generalmente será más difícil si hago eso. Así que termino haciendo algo como esto:

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

Me estremezco por dentro cada vez que tengo que escribir eso, ¿hay una forma más limpia de mover un personaje con valores de velocidad no enteros o me quedaré atrapado haciendo esto para siempre?

Acumulador
fuente
11
¿Ha considerado hacer que su unidad entera sea más pequeña (por ejemplo, 1/10 de una unidad de visualización) y luego 2.5 se traduce en 25, y aún puede tratarla como un número entero para todas las comprobaciones y tratar cada fotograma de manera consistente.
DMGregory
66
Puede considerar la adopción del algoritmo de línea de Bresenham que puede implementarse utilizando solo enteros.
n0rd
1
Esto se hace comúnmente en consolas antiguas de 8 bits. Vea artículos como este para ver un ejemplo de cómo se implementa el movimiento de subpíxeles con métodos de punto fijo: tasvideos.org/GameResources/NES/BattleOfOlympus.html
Lucas

Respuestas:

13

Bresenham

En los viejos tiempos, cuando la gente todavía escribía sus propias rutinas de video básicas para dibujar líneas y círculos, no era extraño usar el algoritmo de línea de Bresenham para eso.

Bresenham resuelve este problema: desea dibujar una línea en la pantalla que mueva los dxpíxeles en la dirección horizontal y al mismo tiempo abarque los dypíxeles en la dirección vertical. Hay un carácter "flotante" inherente a las líneas; incluso si tienes píxeles enteros, terminas con inclinaciones racionales.

Sin embargo, el algoritmo debe ser rápido, lo que significa que solo puede usar aritmética de enteros; y también se escapa sin multiplicación o división, solo suma y resta.

Puede adaptar eso para su caso:

  • Su "dirección x" (en términos del algoritmo de Bresenham) es su reloj.
  • Tu "dirección y" es el valor que deseas incrementar (es decir, la posición de tu personaje; cuidado, esta no es realmente la "y" de tu sprite o lo que sea en la pantalla, más bien un valor abstracto)

"x / y" aquí no es la ubicación en la pantalla, sino el valor de una de sus dimensiones en el tiempo. Obviamente, si su sprite se ejecuta en una dirección arbitraria a través de la pantalla, tendrá varios Bresenhams ejecutándose por separado, 2 para 2D, 3 para 3D.

Ejemplo

Digamos que quieres mover a tu personaje en un movimiento simple de 0 a 25 a lo largo de uno de tus ejes. Como se mueve con la velocidad 2.5, llegará allí en el cuadro 10.

Esto es lo mismo que "dibujar una línea" de (0,0) a (10,25). Toma el algoritmo de línea de Bresenham y déjalo correr. Si lo haces bien (y cuando lo estudias, rápidamente se hará claro cómo hacerlo bien), entonces generará 11 "puntos" para ti (0,0), (1,2), (2, 5), (3,7), (4,10) ... (10,25).

Consejos sobre la adaptación

Si buscas en Google ese algoritmo y encuentras algún código (Wikipedia tiene un tratado bastante extenso), hay algunas cosas que debes tener en cuenta:

  • Obviamente funciona para todo tipo de dxy dy. Sin embargo, está interesado en un caso específico (es decir, nunca lo tendrá dx=0).
  • La aplicación habitual tendrá varios casos diferentes de los cuadrantes de la pantalla, dependiendo de si dxy dyson positivas, negativas, y también si abs(dx)>abs(dy)o no. Por supuesto, también elige lo que necesita aquí. Debe asegurarse especialmente de que la dirección que aumenta con 1cada tic sea siempre la dirección de su "reloj".

Si aplica estas simplificaciones, el resultado será muy simple y eliminará por completo cualquier real.

AnoE
fuente
1
Esta debería ser la respuesta aceptada. Habiendo programado juegos en el C64 en los 80 y fractales en la PC en los 90, todavía me da miedo usar el punto flotante siempre que puedo evitarlo. O, por supuesto, con las FPU omnipresentes en los procesadores actuales, el argumento del rendimiento es principalmente discutible, pero la aritmética de coma flotante todavía necesita muchos más transistores, lo que consume más energía, y muchos procesadores apagarán sus FPU por completo mientras no estén en uso. Por lo tanto, evitar el punto flotante hará que sus usuarios de dispositivos móviles le agradezcan por no chupar sus baterías tan rápido.
Guntram Blohm apoya a Monica el
@GuntramBlohm La respuesta aceptada también funciona perfectamente bien cuando se usa el punto fijo, lo cual creo que es una buena manera de hacerlo. ¿Cómo te sientes acerca de los números de punto fijo?
leetNightshade
Cambié esto a la respuesta aceptada después de descubrir que así es como lo hicieron en los días de 8 y 16 bits.
Acumulador
26

Hay una excelente manera de hacer exactamente lo que quieres.

Además de una floatvelocidad, necesitará tener una segunda floatvariable que contendrá y acumulará una diferencia entre la velocidad real y la velocidad redondeada . Esta diferencia se combina con la velocidad misma.

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

Salida:

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

HolyBlackCat
fuente
55
¿Por qué prefiere esta solución en lugar de usar flotante (o punto fijo) para velocidad y posición y solo redondear la posición a enteros enteros al final?
CodesInChaos
@CodesInChaos No prefiero mi solución sobre esa. Cuando estaba escribiendo esta respuesta no lo sabía.
HolyBlackCat
16

Utilice valores flotantes para movimiento y valores enteros para colisión y renderizado.

Aquí hay un ejemplo:

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

Cuando te mueves, usas lo move()que acumula las posiciones fraccionarias. Pero la colisión y la representación pueden ocuparse de posiciones integrales mediante el uso de la getPosition()función.

congusbongus
fuente
Tenga en cuenta que en el caso de un juego en red, el uso de tipos de punto flotante para la simulación del mundo puede ser complicado. Ver, por ejemplo, gafferongames.com/networking-for-game-programmers/… .
liori
@liori Si tiene una clase de punto fijo que actúa como un reemplazo directo para flotante, ¿eso no resuelve esos problemas principalmente?
leetNightshade
@leetNightshade: depende de la implementación.
liori
1
Diría que el problema no existe en la práctica, ¿qué hardware capaz de ejecutar juegos en red modernos no tiene flotadores IEEE 754?
Sopel