Valores de velocidade não inteiros - existe uma maneira mais limpa de fazer isso?

21

Frequentemente, desejarei usar um valor de velocidade como 2,5 para mover meu personagem em um jogo baseado em pixels. A detecção de colisão geralmente será mais difícil se eu fizer isso. Então, acabo fazendo algo assim:

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

Eu me encolho por dentro toda vez que tenho que escrever isso, existe uma maneira mais limpa de mover um personagem com valores de velocidade não inteiros ou continuarei preso a fazê-lo para sempre?

Acumulador
fonte
11
Você já pensou em tornar sua unidade inteira menor (por exemplo, 1/10 de uma unidade de exibição) e 2,5 traduz para 25, e ainda pode tratá-la como um número inteiro para todas as verificações e tratar todos os quadros de maneira consistente.
DMGregory
6
Você pode considerar a adoção do algoritmo de linha de Bresenham, que pode ser implementado usando apenas números inteiros.
N0rd
1
Isso geralmente é feito em consoles antigos de 8 bits. Veja artigos como este para obter um exemplo de como o movimento de subpixel é implementado com métodos de ponto fixo: tasvideos.org/GameResources/NES/BattleOfOlympus.html
Lucas

Respostas:

13

Bresenham

Antigamente, quando as pessoas ainda escreviam suas próprias rotinas básicas de vídeo para desenhar linhas e círculos, não era incomum usar o algoritmo de linha de Bresenham para isso.

Bresenham resolve esse problema: você deseja desenhar uma linha na tela que mova os dxpixels na direção horizontal e ao mesmo tempo estenda os dypixels na direção vertical. Existe um caractere "flutuante" inerente às linhas; mesmo se você tiver pixels inteiros, você acaba com inclinações racionais.

O algoritmo precisa ser rápido, o que significa que ele pode usar apenas aritmética inteira; e também foge sem multiplicação ou divisão, apenas adição e subtração.

Você pode adaptar isso para o seu caso:

  • Sua "direção x" (em termos do algoritmo de Bresenham) é o seu relógio.
  • Sua "direção y" é o valor que você deseja incrementar (ou seja, a posição do seu personagem - cuidado, este não é realmente o "y" do seu sprite ou o que quer que seja na tela, mais um valor abstrato)

"x / y" aqui não é o local na tela, mas o valor de uma de suas dimensões no tempo. Obviamente, se o seu sprite estiver sendo executado em uma direção arbitrária na tela, você terá vários Bresenhams executando separadamente, 2 para 2D, 3 para 3D.

Exemplo

Digamos que você queira mover seu personagem em um movimento simples de 0 a 25 ao longo de um dos seus eixos. Como está se movendo com a velocidade 2.5, ele chegará lá no quadro 10.

É o mesmo que "desenhar uma linha" de (0,0) a (10,25). Pegue o algoritmo de linha de Bresenham e deixe correr. Se você fizer o que é certo (e quando estudá-lo, rapidamente ficará claro como o faz), gerará 11 "pontos" para você (0,0), (1,2), (2, 5), (3,7), (4,10) ... (10,25).

Dicas sobre adaptação

Se você pesquisar esse algoritmo no Google e encontrar algum código (a Wikipedia possui um tratado bastante grande), há algumas coisas que você precisa observar:

  • Obviamente, funciona para todos os tipos de dxe dy. Você está interessado em um caso específico (por exemplo, você nunca terá dx=0).
  • A implementação de costume terá vários casos diferentes para os quadrantes da tela, dependendo se dxe dysão positivos, negativos, e também se abs(dx)>abs(dy)ou não. É claro que você também escolhe o que precisa aqui. Você deve ter certeza de que a direção que é aumentada a 1cada escala é sempre a direção do "relógio".

Se você aplicar essas simplificações, o resultado será muito simples e se livrará completamente de quaisquer reais.

AnoE
fonte
1
Essa deve ser a resposta aceita. Tendo programado jogos no C64 nos anos 80 e fractais nos PCs nos anos 90, ainda me arrependo ao usar o ponto flutuante onde posso evitá-lo. Ou, é claro, com as FPUs onipresentes nos processadores de hoje, o argumento de desempenho é geralmente discutível, mas a aritmética de ponto flutuante ainda precisa de muito mais transistores, consumindo mais energia, e muitos processadores desligam suas FPUs completamente enquanto não estão em uso. Portanto, evitar o ponto flutuante fará com que os usuários de dispositivos móveis agradeçam por não sugarem suas baterias tão rapidamente.
Guntram Blohm apoia Monica
@GuntramBlohm A resposta aceita também funciona perfeitamente bem ao usar o Fixed Point, o que eu acho que é uma boa maneira de fazê-lo. Como você se sente sobre números de pontos fixos?
LeetNightshade
Alterou isso para a resposta aceita depois de descobrir que é assim que eles fazem nos dias de 8 e 16 bits.
acumulador
26

Existe uma ótima maneira de fazer exatamente o que você deseja.

Além de uma floatvelocidade, você precisará ter uma segunda floatvariável que conterá e acumulará uma diferença entre a velocidade real e a velocidade arredondada . Essa diferença é então combinada com a própria velocidade.

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

Saída:

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

HolyBlackCat
fonte
5
Por que você prefere esta solução ao invés de usar float (ou ponto fixo) para velocidade e posição e arredondar a posição para números inteiros inteiros no final?
CodesInChaos
@CodesInChaos Não prefiro minha solução a essa. Quando escrevi esta resposta, não sabia.
precisa saber é o seguinte
16

Use valores flutuantes para movimento e valores inteiros para colisão e renderização.

Aqui está um exemplo:

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

Quando você se move, você usa o move()que acumula as posições fracionárias. Mas a colisão e a renderização podem lidar com posições integrais usando a getPosition()função

congusbongus
fonte
Observe que, no caso de um jogo em rede, o uso de tipos de ponto flutuante para simulação mundial pode ser complicado. Veja, por exemplo, gafferongames.com/networking-for-game-programmers/… .
Liori
@liori Se você tem uma classe de ponto fixo que age como um substituto para o float, isso não resolve principalmente esses problemas?
LeitNightshade
@leetNightshade: depende da implementação.
Liori 07/12/16
1
Eu diria que o problema é inexistente na prática, que hardware capaz de executar jogos em rede modernos não possui flutuadores IEEE 754 ???
Sopel