Нецелые значения скорости - есть ли более чистый способ сделать это?

21

Часто я хочу использовать значение скорости, например, 2,5, для перемещения моего персонажа в пиксельной игре. Обнаружение столкновений, как правило, будет более трудным, если я это сделаю. В итоге я делаю что-то вроде этого:

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

Каждый раз, когда мне приходится писать, я съеживаюсь изнутри, есть ли более чистый способ перемещения персонажа с нецелыми значениями скорости, или я застряну, делая это навсегда?

аккумуляторный
источник
11
Рассматривали ли вы уменьшение целочисленной единицы (например, 1/10 от единицы отображения), затем 2,5 переводится в 25, и вы все равно можете рассматривать его как целое число для всех проверок и обрабатывать каждый кадр последовательно.
DMGregory
6
Вы можете рассмотреть возможность принятия алгоритма линии Брезенхэма, который может быть реализован с использованием только целых чисел.
n0rd
1
Обычно это делается на старых 8-битных консолях. См. Статьи, подобные этой, для примера того, как субпиксельное движение реализуется с помощью методов с фиксированной точкой: tasvideos.org/GameResources/NES/BattleOfOlympus.html
Лукас

Ответы:

13

Bresenham

В старые времена, когда люди все еще писали свои собственные базовые видео-процедуры для рисования линий и кругов, было весьма неслыханно использовать для этого алгоритм линий Брезенхэма.

Bresenham решает эту проблему: вы хотите нарисовать линию на экране, которая перемещает dxпиксели в горизонтальном направлении и в то же время охватывает dyпиксели в вертикальном направлении. В строках есть свой "плавный" характер; даже если у вас целочисленные пиксели, вы получите рациональные наклонности.

Хотя алгоритм должен быть быстрым, это означает, что он может использовать только целочисленную арифметику; и это также уходит без умножения или деления, только сложение и вычитание.

Вы можете адаптировать это для вашего случая:

  • Ваше «направление х» (с точки зрения алгоритма Брезенхэма) - ваши часы.
  • Ваше «направление y» - это значение, которое вы хотите увеличить (т. Е. Положение вашего персонажа - осторожно, на самом деле это не «y» вашего спрайта или чего-либо еще на экране, а скорее абстрактное значение)

«x / y» - это не местоположение на экране, а значение одного из ваших измерений во времени. Очевидно, что если ваш спрайт движется в произвольном направлении по экрану, у вас будет несколько брезенхамов, работающих отдельно, 2 для 2D, 3 для 3D.

пример

Допустим, вы хотите переместить своего персонажа простым движением от 0 до 25 вдоль одной из ваших осей. Поскольку он движется со скоростью 2,5, он будет расти там на 10 кадре.

Это то же самое, что «рисование линии» от (0,0) до (10,25). Возьмите алгоритм Брезенхема и дайте ему поработать. Если вы сделаете это правильно (и когда вы изучите это, очень быстро станет ясно, как вы это делаете правильно), то это сгенерирует для вас 11 «очков» (0,0), (1,2), (2, 5), (3,7), (4,10) ... (10,25).

Советы по адаптации

Если вы заглянете в этот алгоритм и найдете какой-то код (на Википедию заключен довольно большой договор), есть несколько вещей, на которые нужно обратить внимание:

  • Это очевидно работает для всех видов dxи dy. Вы заинтересованы в одном конкретном случае, хотя (то есть, вы никогда не будете иметь dx=0).
  • Обычная реализация будет иметь несколько различных случаев для квадранта на экране, в зависимости от того , dxи dyявляются положительными, отрицательными, а также того abs(dx)>abs(dy)или нет. Вы, конечно, также выберите то, что вам нужно здесь. Вы должны быть особенно уверены, что направление, которое увеличивается с 1каждым тактом, всегда является вашим «часовым» направлением.

Если вы примените эти упрощения, результат будет действительно очень простым и полностью избавит вас от любых реалов.

Anoe
источник
1
Это должен быть принятый ответ. Запрограммировав игры на C64 в 80-х и фракталы на ПК в 90-х, я все еще недоволен использованием плавающей запятой везде, где я могу этого избежать. Конечно же, поскольку в современных процессорах FPU используются повсеместно, аргумент производительности в основном не имеет значения, но арифметике с плавающей запятой по-прежнему требуется гораздо больше транзисторов, потребляющих больше энергии, и многие процессоры полностью отключают свои FPU, когда они не используются. Таким образом, избегая с плавающей запятой, ваши мобильные пользователи будут благодарны вам за то, что вы не разряжаете свои батареи так быстро.
Гунтрам Блом поддерживает Монику
@GuntramBlohm Принятый ответ прекрасно работает и при использовании Fixed Point, что, я думаю, является хорошим способом сделать это. Как вы относитесь к номерам с фиксированной точкой?
leetNightshade
Изменив это на принятый ответ, узнав, как они это делали в 8-битный и 16-битный дни.
Аккумулятор
26

Есть отличный способ сделать именно то, что вы хотите.

В дополнение к floatскорости вам понадобится вторая floatпеременная, которая будет содержать и накапливать разницу между реальной скоростью и округленной скоростью. Эта разница затем объединяется с самой скоростью.

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

Выход:

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

HolyBlackCat
источник
5
Почему вы предпочитаете это решение вместо использования числа с плавающей запятой (или фиксированной точки) для скорости и положения и округления только целых чисел в самом конце?
CodesInChaos
@CodesInChaos Я не предпочитаю свое решение этому. Когда я писал этот ответ, я не знал об этом.
HolyBlackCat
16

Используйте плавающие значения для движения и целочисленные значения для столкновения и рендеринга.

Вот пример:

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

Когда вы двигаетесь, вы используете, move()что накапливает дробные позиции. Но столкновение и рендеринг могут иметь дело с интегральными позициями с помощью getPosition()функции.

congusbongus
источник
Обратите внимание, что в случае сетевой игры использование типов с плавающей запятой для симуляции мира может быть сложным. Смотрите, например, gafferongames.com/networking-for-game-programmers/… .
Лиори
@liori Если у вас есть класс с фиксированной запятой, который действует как быстрая замена поплавка, разве это не решает в основном эти проблемы?
leetNightshade
@leetNightshade: зависит от реализации.
Лиори
1
Я бы сказал, что проблема не существует на практике, какое оборудование, способное запустить современную сетевую игру, не имеет IEEE 754 float ???
Sopel