gui_library.h – наша библиотека: окно, пульт, графики. <vector> нужен для списка точек мишени, <cmath> – для sin, cos и sqrt.
Свой заголовок пишут в кавычках, чужой – в угловых скобках. Разница только в том, где компилятор ищет файл.
Откройте Task_0/main.cpp. В нём около двухсот строк, из них больше половины – комментарии. Программа устроена так же, как все задачи курса, и порядок частей всегда один:
main – сборка пульта и запуск.Разберём по порядку. Если какой-то приём языка встретится впервые, он объяснён в разделе «Основы C++».
#include "gui_library.h"
#include <vector>
#include <cmath>
gui_library.h – наша библиотека: окно, пульт, графики. <vector> нужен для списка точек мишени, <cmath> – для sin, cos и sqrt.
Свой заголовок пишут в кавычках, чужой – в угловых скобках. Разница только в том, где компилятор ищет файл.
const float g = 9.8f; // ускорение свободного падения, м/с^2
const float PI = 3.14159265f;
// Мишень – отрезок между двумя точками.
const float TARGET_X1 = 8.5f, TARGET_Y1 = 3.0f;
const float TARGET_X2 = 10.5f, TARGET_Y2 = 3.5f;
const int TARGET_POINTS = 50;
const float HIT_RADIUS = 0.15f;
const char* PLOT = "Полёт тела";
Слово const означает «изменить нельзя». Если где-то в программе случайно написать g = 5;, компилятор откажется собирать файл. Это дешёвая страховка: ошибка ловится до запуска.
Почему это именно константы, а не ручки в пульте? Потому что они задают условие задачи, а не то, что мы подбираем. Скорость и угол мы подбираем – они в пульте. Положение мишени и ускорение свободного падения не подбираем – они в коде. Захотите изменить мишень – меняете эти строки и пересобираете программу.
Отдельно про PLOT. Имя графика нужно программе пять раз: при создании и при каждом рисовании. Можно было бы писать строку "Полёт тела" пять раз, но тогда опечатка в одном месте не была бы ошибкой компиляции – библиотека просто не нашла бы такой график и молча ничего не нарисовала. С константой опечатка невозможна: имя PLOT компилятор проверяет.
std::vector<float> target_x; // координаты X точек мишени
std::vector<float> target_y; // координаты Y точек мишени
std::vector<float> ground_x = {-0.1f, 10.f};
std::vector<float> ground_y = { 0.0f, 0.f};
void make_target() {
target_x.clear();
target_y.clear();
for (int i = 0; i <= TARGET_POINTS; i++) {
float k = (float)i / TARGET_POINTS; // доля пути: от 0 до 1
target_x.push_back(TARGET_X1 + k * (TARGET_X2 - TARGET_X1));
target_y.push_back(TARGET_Y1 + k * (TARGET_Y2 - TARGET_Y1));
}
}
Земля – это всего две точки: библиотека соединит их отрезком, и получится горизонтальная линия $y = 0$. Список в фигурных скобках сразу заполняет вектор.
Мишень строится циклом. Переменная k пробегает значения от 0 до 1 – это доля пути вдоль отрезка. Тогда
$$x_i = x_1 + k\,(x_2 - x_1)$$
даёт точку, отстоящую от начала на долю $k$. При $k = 0$ получаем первый конец, при $k = 1$ – второй, при $k = 0.5$ – середину. Этот приём называется линейной интерполяцией и встречается всюду, где надо равномерно расставить точки между двумя значениями.
Главная ловушка этой функции – в записи (float)i. Если убрать скобки и написать float k = i / TARGET_POINTS;, то i и TARGET_POINTS – оба целые, деление пойдёт по целочисленным правилам, и k будет равно нулю для всех $i$ от 0 до 49 и единице при $i = 50$. Вся мишень схлопнется в две точки на концах. Компилятор не возразит – с точки зрения языка код верен. Подробнее про эту ловушку – в статье «Арифметика и математика».
Цикл идёт i <= TARGET_POINTS, то есть до 50 включительно – точек получается 51, а отрезков между ними ровно 50.
float t; // время от начала полёта, с
float x, y; // текущие координаты тела, м
float V, alpha; // начальная скорость (м/с) и угол (градусы)
float x_0, y_0; // точка старта, м
bool flying = false; // тело ещё летит?
bool hit = false; // попали в мишень?
Эти переменные объявлены снаружи всех функций, и это не небрежность. Расчётная функция вызывается заново каждый кадр; переменная, объявленная внутри неё, родилась бы в начале кадра и умерла в конце, не донеся значение до следующего. А нам нужно именно копить: время растёт, координаты меняются. Подробно об этом – в статье «Функции и главный цикл».
Две переменные типа bool хранят, что происходит: летит или уже нет, попали или нет. Из них складывается всё поведение программы – по сути, это состояние задачи.
Заметьте: flying и hit получили начальные значения прямо при объявлении, а t, x, y – нет. Так можно только потому, что первым делом в main вызывается restart(), которая их всё равно заполнит. В общем случае читать переменную, которой не присвоили значение, – ошибка, и найти её тяжело: программа работает, но считает мусор.
float radians(float degrees) {
return degrees * PI / 180.f;
}
Функция в одну строку, и всё же она заведена отдельно. Почему: перевод нужен дважды (для синуса и для косинуса), а формула, написанная дважды, – это два места, где можно ошибиться, и два места, которые надо править при изменении.
Если забыть про перевод и подставить в sin градусы, программа соберётся и запустится. Угол 30 будет понят как 30 радиан – это почти пять полных оборотов, и синус от него равен $-0.988$. Тело полетит вниз. Ошибку видно только на картинке.
bool touches_target(float body_x, float body_y) {
for (size_t i = 0; i < target_x.size(); i++) {
float dx = body_x - target_x[i];
float dy = body_y - target_y[i];
float distance = sqrt(dx * dx + dy * dy);
if (distance < HIT_RADIUS)
return true;
}
return false;
}
Функция перебирает все точки мишени и для каждой считает расстояние до тела по теореме Пифагора. Как только нашлась хоть одна ближе HIT_RADIUS – return true, и функция заканчивается немедленно, не досматривая остальные точки. Смысла досматривать нет: ответ уже известен.
Строка return false в самом конце срабатывает, только если цикл прошёл целиком и ничего не нашёл.
Тип size_t – это «целое для размеров и индексов». Метод .size() возвращает именно его, и если завести счётчик как int, компилятор выдаст предупреждение о сравнении разных типов. Ошибки не будет, но предупреждения лучше не копить: в куче безобидных легко пропустить важное.
void restart() {
V = get_float_param("Начальная скорость");
alpha = get_float_param("Угол броска");
x_0 = get_float_param("Старт x");
y_0 = get_float_param("Старт y");
t = 0.f;
x = x_0;
y = y_0;
flying = true;
hit = false;
clear_plot_history(PLOT);
add_plot_history_point(PLOT, x, y, "Траектория", BLUE, 1.f, 2000);
set_bool_param("Пауза", false);
}
Здесь видно, зачем вообще нужна кнопка. Скорость и угол читаются из пульта один раз, в начале полёта, и дальше не перечитываются. Поэтому крутить ручку «Угол броска» во время полёта бесполезно – тело уже летит с прежним углом. Чтобы новый угол вступил в силу, нужно нажать «Заново».
Это осознанное решение, а не недоделка: скорость в середине полёта меняться не должна, иначе получится не бросок, а ракета.
Сравните с шагом по времени – он читается внутри расчётной функции, то есть каждый кадр. Его как раз можно менять на лету, и эффект виден сразу.
clear_plot_history стирает старую траекторию. Без этой строки новый полёт рисовался бы поверх всех предыдущих – иногда это полезно, и в заданиях есть пункт, где именно так и нужно.
void calculation_function() {
bool pause = get_bool_param("Пауза");
if (flying && !pause) {
float dt = get_float_param("Шаг по времени");
t += dt;
x = x_0 + V * cos(radians(alpha)) * t;
y = y_0 + V * sin(radians(alpha)) * t - g * t * t / 2.f;
if (touches_target(x, y)) {
hit = true;
flying = false;
}
if (y <= 0.f) {
y = 0.f;
flying = false;
}
add_plot_history_point(PLOT, x, y, "Траектория", BLUE, 1.f, 2000);
}
Условие if (flying && !pause) читается как «если тело летит и пауза не нажата». Знак ! означает «не», && – «и». Когда полёт кончился, весь блок пропускается: время стоит, точки не добавляются, картинка замирает.
Дальше идут две проверки конца полёта. Порядок между ними важен: сначала мишень, потом земля. Если бы тело задело мишень ровно на уровне земли, при обратном порядке попадание не засчиталось бы.
Строка y = 0.f; внутри второй проверки чуть-чуть подправляет физику: без неё тело остановилось бы на той глубине, на которую ушло под землю за последний шаг. Мелочь, но именно из таких мелочей складывается ощущение, что программа работает правильно.
Сравнение y <= 0.f – это сравнение вещественного числа, и оно здесь безопасно, потому что мы сравниваем с порогом, а не на точное равенство. Писать if (y == 0.f) было бы ошибкой: тело почти наверняка перескочит через ноль, ни разу не совпав с ним в точности.
// --- рисуем картину заново каждый кадр ---
clear_plot(PLOT);
add_plot_line(PLOT, ground_x, ground_y, "Земля", GREEN, 1.f);
add_plot_line(PLOT, target_x, target_y, "Мишень", WHITE, 3.f);
add_plot_point(PLOT, x, y, "Тело", RED, 5.f);
if (hit)
add_plot_point(PLOT, x, y, "Попадание", YELLOW, 10.f);
}
Вторая половина функции – рисование, и она вынесена из-под if (flying && !pause): картинку надо перерисовывать каждый кадр, даже когда полёт кончился. Иначе на паузе экран опустел бы.
В библиотеке две разные вещи:
add_plot_line, add_plot_point) живут до следующего clear_plot. Их надо добавлять заново каждый кадр – поэтому земля, мишень и тело перечислены здесь;add_plot_history_point) копится сама и рисуется, пока её не очистят. Поэтому траектория остаётся на экране, хотя добавляли мы по точке за кадр.Строка clear_plot(PLOT) стирает объекты прошлого кадра и не трогает историю. Без неё каждый кадр добавлял бы ещё одну копию земли и мишени поверх старых – через минуту работы их накопились бы тысячи, и программа начала бы тормозить.
int main() {
if (!init_gui_library("Задача 0. Движение тела под углом", 1200, 800))
return -1;
add_bool_param("Пауза", false);
add_button_param("Заново", restart);
add_float_param("Начальная скорость", 8.0f);
add_float_param("Угол броска", 30.0f);
add_float_param("Шаг по времени", 0.01f);
add_float_param("Старт x", 0.0f);
add_float_param("Старт y", 3.0f);
create_plot(PLOT, -0.1f, 11.f, -0.1f, 5.f, 800, 700);
set_plot_axes(PLOT, "x, м", "y, м");
make_target();
restart();
set_calculation_function(calculation_function);
set_auto_layout();
run_gui_library();
return 0;
}
Порядок здесь не случайный:
calculation_function – это «вот функция», а calculation_function() – «вызови её прямо сейчас и отдай результат». Нам нужно первое;run_gui_library() не возвращается, пока не закроют окно. Всё, что вы напишете после неё, выполнится только при выходе.Обратите внимание: restart() вызывается до run_gui_library(), и к этому моменту пульт уже собран – иначе get_float_param не нашла бы ни одной ручки и вернула бы нули.
Короткая карта на случай, когда нужно что-то поправить, а лезть через весь файл не хочется.
| Что хотите изменить | Куда смотреть |
|---|---|
| положение и наклон мишени | TARGET_X1, TARGET_Y1, TARGET_X2, TARGET_Y2 |
| «толщину» тела, попасть легче или труднее | HIT_RADIUS |
| ускорение свободного падения | g |
| видимую область графика | create_plot в main |
| набор ручек в пульте | блок add_*_param в main |
| саму физику полёта | две строки в calculation_function |
| что рисуется на экране | вторая половина calculation_function |