Это записанные на C++ формулы кинематики:
Вся физика полёта тела умещается в две строки:
Вся физика полёта тела умещается в две строки:
x = x_0 + V * cos(radians(alpha)) * t;
y = y_0 + V * sin(radians(alpha)) * t - g * t * t / 2.f;
Это записанные на C++ формулы кинематики:
Похоже настолько, что кажется, будто переводить формулу в код – дело механическое. Почти так и есть, но у этого перевода есть ровно одна ловушка, на которую попадаются все. К ней и придём.
Знаки те же, что в математике: +, -, *, /. Умножение нельзя опускать: 2t – ошибка, надо 2 * t. Возведения в степень знаком нет вовсе, для него есть функция.
Приоритет обычный: сначала умножение и деление, потом сложение и вычитание, скобки главнее всего. В строке для y сначала посчитается radians(alpha), затем синус, затем умножение на V и на t; отдельно посчитается g * t * t / 2.f, и только в конце произойдут сложение с y_0 и вычитание.
Вот та самая ловушка. Посмотрите на строку и скажите, что окажется в dt:
float dt = 1 / 60;
В dt окажется ноль. Не 0.0167, а именно ноль.
Причина: 1 и 60 – целые числа. Для целых чисел деление в C++ тоже целое: оно даёт целую часть частного и выбрасывает остаток. 1 / 60 – это «сколько раз 60 помещается в 1», то есть ноль. И уже этот ноль кладётся в дробную переменную.
Компилятор не жалуется: он не знает, что вы имели в виду. Программа запустится, время не будет идти, а картинка замрёт – и искать причину вы будете в физике, а не в этой строке.
float dt = 1.0f / 60.0f; // 0.0167 – правильно
float dt = 1 / 60; // 0 – ловушка
float dt = 1.0f / 60; // 0.0167 – тоже правильно:
// хватает одного дробного участника
Правило, которое стоит запомнить дословно: если в делении оба числа целые, результат будет целым. Достаточно сделать дробным хотя бы одно – и деление станет обычным.
То же касается переменных. Если int steps = 7; и вы пишете steps / 2, получите 3, а не 3.5.
Отсюда и / 2.f в формуле для y. Строго говоря, там хватило бы и / 2: слева от деления стоит g * t * t, а это уже дробное число, и деление будет дробным. Но привычка писать 2.f дешевле, чем каждый раз проверять глазами, что стоит слева.
Прибавить к переменной и положить обратно приходится постоянно, поэтому для этого есть короткая форма:
| Запись | То же самое |
|---|---|
t += dt; |
t = t + dt; |
v -= 0.1f; |
v = v - 0.1f; |
x *= 2.0f; |
x = x * 2.0f; |
i++; |
i = i + 1; |
В задачах курса вы увидите в основном +=: именно так на каждом шаге растёт время и меняется скорость.
Синус, косинус, корень и прочее живут в стандартной библиотеке. Чтобы ими пользоваться, в начале файла должна быть строка:
#include <cmath>
Что понадобится в задачах курса:
| Функция | Что делает |
|---|---|
sin(x), cos(x) |
синус и косинус |
sqrt(x) |
квадратный корень |
pow(x, n) |
возведение в степень |
fabs(x) |
модуль дробного числа |
exp(x), log(x) |
экспонента и натуральный логарифм |
Углы – только в радианах. sin(30) даст не 0.5, а синус тридцати радиан, то есть почти минус единицу. Если угол задан в градусах, его надо перевести.
const float PI = 3.14159265f;
float radians(float degrees) {
return degrees * PI / 180.f;
}
// дальше в расчёте:
x = x_0 + V * cos(radians(alpha)) * t;
Число «пи» здесь объявлено своё. В <cmath> есть готовое M_PI, но пользоваться им напрямую опасно, поэтому рядом с ним обычно пишут вот такую защиту:
#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif
Читается как «если M_PI ещё не определено – определим сами». Дело в том, что M_PI не входит в стандарт языка: на Linux она есть почти всегда, на Windows – не во всех настройках. Эти три строки делают код одинаково рабочим везде.
Последнее, но важное для численного счёта. float хранит число приближённо. Поэтому такая проверка почти наверняка не сработает:
if (t == 1.0f) { ... } // почти никогда не выполнится
Время растёт шагами: t += dt. Даже при dt = 0.01 сотня сложений даст не ровно 1.0, а 0.99999994 или 1.0000001 – каждое сложение округляется, и погрешности копятся. Проверка на точное равенство просто не сработает, а тело, скажем, не остановится там, где должно.
Сравнивать дробные надо неравенствами: if (t >= 1.0f). Знак равенства оставьте для целых чисел и bool.
Поэтому касание земли в Task_0 проверяется так:
if (y <= 0.f) { ... } // а не y == 0.f
Ровно в ноль координата не попадёт почти никогда: за один шаг тело перепрыгивает через землю и оказывается чуть ниже неё.
Формулу вы теперь запишете. Осталось понять, кто и когда её считает. Об этом следующая статья – «Функции и главный цикл».