Лабораторная работа №2 «Моделирование физического взаимодействия объектов» В данной работе рассмотрим основы моделирования физического взаимодействия объектов на примере шаров. Шары должны взаимодействовать со стенками окна и друг с другом. Моделировать столкновения можно двумя способами. Velocity-based – подход. Подход, основанный на прогнозировании соударений шаров путем расчета момента соударений на основании заданных векторов скоростей. Данный подход помогает получить наибольшую точность моделирования, но требует более сложных расчетов и более высокую вычислительную мощь. Position-based – подход. Подход, основанный на мгновенном положении шаров и их расталкивании при столкновении в заданном направлении. Данный подход гораздо проще для моделирования, однако имеет ряд недостатков. Одним из недостатков является то, что время между двумя кадрами (конкретнее – расчетами положения) должно быть фиксированным. Другим недостатком является то, что для быстродвижущихся шаров (быстродвижущимися шарами считаются шары, которые за один кадр моделирования проходят расстояние сопоставимое с собственным радиусом). В работе для простоты будем использовать position-based подход. Для экономии времени разработка проводится на базе созданного проекта LabWork2_Initial, который содержит шаблон оконного приложения, в котором установлена обработка сообщений WM_DESTROY и WM_SIZE (см. лабораторную работу №1). Определим необходимое содержимое класса, описывающего объект шара: 1. Позиция шара в мире (окне) (x, y); 2. Радиус шара (r); 3. Вектор скорости шара (v_x, v_y); 4. Границы окна (rect). Опишем класс (в файле CBall.h, реализация будет CBall.cpp): Для того, чтобы не было проблем с определением типов системы Windows, не забываем подключить заголовочные файлы windows.h и windowsx.h. Добавим метод Create для задания параметров: Его реализация будет выглядеть так: Продумаем, какие еще методы жизненно необходимы классу шара. Прежде всего это методы Move – для перемещения шара и Draw – для его отрисовки, также реализуем метод SetBounds для установки новых границ окна. Реализуем метод Move по аналогии с лабораторной работой №1. (Не забудьте добавить описание метода в опеределение класса!): Отличие данного метода от его реализации в предыдущей работе в том, что на вход подается уже расчитанная временная разница между кадрами. В данной реализации пока проверяется только столкновение шара со стенками окна. Реализуем метод SetBounds: Реализуем метод Draw, принимающий на вход контекст рисования: Простейшая реализация класса шара готова. Добавим ее использование в основное приложение. Добавим глобальную переменную типа CBall: Добавим инициализацию параметров шара в функции WinMain непосредственно перед запуском цикла обработки оконных сообщений: В обработку сообщения WM_SIZE добавляем вызов установки новых границ. Обработчик будет выглядеть так: В обработку сообщения WM_PAINT добавляем вызов отрисовки. Обработчик будет выглядеть так: Остается самая главная задача – реализовать OnIdle так, чтобы происходило движение шара. Задачу расчета времени между кадрами мы переложили на главное приложение. Поэтому в глобальных переменных объявим переменную, отвечающую за время запуска предыдущего кадра: И начальную инициализацию этого значения в WinMain непосредственно перед стартом цикла обработки сообщений: Функция OnIdle должна получать текущее время, производить перемещение шара и генерировать сообщение о перерисовке: Запустим программу и увидим отскакивающий от стенок шарик. Модифицируем приложение так, чтобы можно было работать с произвольным количеством шариков. Создадим CBallsArray, который будет управлять нашими шарами (файлы BallsArray.h и BallsArray.cpp). Описание класса: Поле count – текущее число шаров в массиве. Поле max_balls – максимально возможное число шаров в массиве. Поле balls – массив шаров. Конструктор принимает в качестве параметра максимально возможное число шаров. Метод Add возвращает указатель на новый добавляемый шар или NULL, если шар добавить нельзя. Реализуем методы: Введем в класс реализацию общих методов для шаров: Move, Draw, SetBounds. Это делается для того, чтобы переложить основную работу по вызову этих методов на класс CBallsArray. Переработаем основную программу так, чтобы она работала не напрямую с шарами, а с массивом шаров. Первым делом избавимся от всего, что работало в основной программе с классом CBall и уберем глобальную переменную CBall ball. Добавим глобальную переменную CBallsArray: Параметр 50 задает максимально допустимое число шаров в массиве. Строку: заменяем на последовательность (для двух шаров): В реализации обработчика сообщения WM_SIZE заменяем строку: на В реализации обработчика сообщения WM_PAINT заменяем строку: на В реализации OnIdle заменяем: на Если запустить приложение, то в результате мы увидим окно с двумя летающими шариками. Модифицируем программу так, чтобы пользователь мог самостоятельно добавлять шары. Допустим, при помощи щелчка левой кнопкой мыши. При щелчке левой кнопкой мыши система отправляет окну сообщение WM_LBUTTONDOWN – при нажатии кнопки и WM_LBUTTONUP – при отпускании кнопки. Если пользователь нажал кнопку и, не отпустив, перевел мышь в другое место, то координаты места нажатия кнопки и текущее положение курсора не будут совпадать. Исходя из логики добавление шара надо делать по событию WM_LBUTTONUP, чтобы координаты добавления шара и положение курсора мыши совпадали. Параметр lParam сообщения WM_LBUTTONUP содержит в младшем слове координату X, а в старшем слове – координату Y курсора мыши в момент отпускания мыши. В обработчике этого сообщения можно добавить в систему еще один шар. Не забываем сделать проверку полученного ball на NULL на случай превышения числа шаров. Запускаем приложение и щелкаем в окне левой кнопкой мыши. К сожалению в данный момент мы не управляем ни направлением перемещения создаваемых шаров, ни их скоростью. Реализуем это следующим образом. При помощи клавиш «влево» и «вправо» мы будем изменять угол скорости шара. При помощи «вверх» и «вниз» будем изменять модуль скорости движения. Перед щелчком мыши клавиатурой задаем параметры, которые будут переданы создаваемому шару. Остается одна проблема, которую надо решить – пользователь не знает и не видит текущих настроек. Реализуем визуальный элемент, который будет располагаться в углу окна и показывать текущие заданные скорость и направление движения. Например, скорость длиной стрелки, а направление – направлением стрелки. Визуальный элемент реализуем в классе CBallSettingsMonitor. Реализуем методы этого элемента: В конструкторе задается скорость и направление движения по умолчанию. Пара методов SpeedUp и SpeedDown увеличивает и уменьшает параметр скорости соответственно. При этом проверяется, если параметр скорости меньше 10 или больше 120, то изменений не происходит. Это сделано для того, чтобы пользователь не смог задать скорость больше максимально допустимой или меньше минимально допустимой. Пара методов AngleUp и AngleDown аналогичным образом работает с направлением движения нового шарика. Так как скорость шарика задается отдельно по x и по y, то производится расчет этих значений исходя из направления и скорости. Для работы с функциями sin и cos не забудьте подключить заголовочный файл math.h. Метод Draw отрисовывает пользовательский элемент в окне. Теперь модифицируем основную программу. Добавим в глобальные переменные наш объект пользовательского элемента (на забыв подключить заголовочный файл): В обработчик WM_PAINT перед EndPaint добавляем вызов: Добавляем обработчик клавиатурных сообщений, вызывая при нажатии клавиш-стрелок соответствующие функции: Обработчик сообщения WM_LBUTTONUP переписываем следующим образом: Запускаем программу и проверяем результат – стрелками выбираем скорость и направление, мышью запускаем новый шар. Добавим реализацию столкновения шаров. Для этого сначала нужно вспомнить немного теории из физики. Центральные столкновения тел Рассмотрим два сферических объекта (шарика) с массами m1 и m2. Предположим, что эти шарики движутся без вращения по одной оси и испытывают центральное упругое соударение. В этом случае закон сохранения импульса запишется в виде: m1v1i + m2v2i = m1v1 + m2v2 где v1i и v2i - начальные скорости каждого объекта, а v1 и v2 - их конечные скорости. Закон сохранения энергии записывается в виде: m1v1i2 / 2 + m2v2i2 / 2 = m1v12 / 2 + m2v22 / 2 Закон сохранения импульса может быть преобразован следующим образом: m1 (v1i - v1) = m2 (v2 - v2i) Также преобразуем выражение для закона сохранения энергии m1 (v1i2 - v12) = m2 (v22 - v2i2) Если разница между начальной и конечной скоростями не равна нулю (то есть столкновение действительно произошло), мы можем разделить второе из двух последних уравнений на первое, что дает: v1i + v1 = v2 + v2i или v1i - v2i = v2 - v1 Другими словами, в одномерном случае упругих столкновений относительная скорость движения объектов после столкновения равняется относительной скорости движения до столкновения. Чтобы получить конечные скорости движения объектов через их начальные скорости и массы, нужно выразить v2 из последнего уравнения и подставить его в уравнение для закона сохранения импульса. Окончательно получаем: v1 = v1i (m1 - m2) / (m1 + m2) + v2i (2 m2) / (m1 + m2) Таким же способом находим выражение для v2 v2 = v1i (2 m1) / (m1 + m2) + v2i (m2 - m1) / (m2 + m1) Далее предположим, что сталкиваются объекты с одинаковой массой, т.е. m1= m2 = m. В этом случае: v1 = v1i (m - m) / (m + m) + v2i (2 m) / (m + m) v2 = v1i (2 m) / (m + m) + v2i (m - m) / (m + m) Окончательно получаем, что v1 = v2i и v2 = v1i Это означает, что в случае центрального упругого соударения объектов с равными массами, они будут просто обмениваться скоростями. Если один из объектов до столкновения покоился, то после столкновения он остановится, а второй объект начнёт движение. При этом скорость движения второго объекта будет равна скорости первого объекта до столкновения. В общем случае центрального и абсолютно упругого столкновения объектов с разными массами, один из которых до столкновения покоился (v2i =0), можно записать следующие выражения для скоростей после удара: v1 = v1i (m1 - m2) / (m1 + m2) v2 = v1i (2 m1) / (m1 + m2) Если масса налетающего шара m1 больше массы покоящегося шара m2 , то v1 и v2 будут положительными и оба шара после столкновения будут двигаться в одном направлении, совпадающем с направлением начального движения налетающего шара. Если же масса налетающего шара m1 меньше массы покоящегося шара m2 , то v1 будет отрицательной, а v2 - положительной, и шары после столкновения будут разлетаться в противоположных направлениях. При этом, т.к. 2 m1>m1 - m2 , то маленький шарик отразиться с большей скоростью. Нецентральное упругое столкновение Картина соударения при нецентральном ударе будет совсем иной. Здесь во время удара имеет место как приближение центров шаров друг к другу вследствие их деформации, так и скольжение поверхности одного шара по поверхности другого. Очевидно, что вследствие скольжения поверхностей возникнут силы трения, которые вместе с упругими силами взаимодействия определят изменение скорости шаров после удара. Кроме того, силы трения вызовут вращение шаров относительно их центров масс. Если силы трения очень малы по сравнению с упругими силами, то действием сил трения можно пренебречь и в этом случае задача о нецентральном столкновении шаров решается достаточно просто. Действительно, соединим центры масс сталкивающихся шаров прямой и разложим скорость каждого шара на нормальную составляющую, направленную вдоль линии центров, и тангенсальную составляющую, перпендикулярную к ней. Так как согласно нашему предположению силы трения отсутствуют, то тангенсальные силы во время столкновения не возникают и, следовательно, тангенсальные скорости шаров изменяться не будут. Нормальные же составляющие скорости после удара можно определить на основании закона сохранения количества движения и закона сохранения энергии таким же путем, как и при центральном ударе. Расчет направления движения шаров после столкновения Расчитаем направление движения двух шаров после нецентрального (общий случай) столкновения. Модифицируем метод Move класса CBallsArray так, чтобы он просматривал список шаров на предмет столкновений и расчитывал скорости после столкновения. Запускаем программу и видим результат. Недостатком данного подхода является то, что возможно взаимное проникновение шаров, которое приводит к их слипанию. Тем не менее для простейшего способа моделирования достигается вполне приемлемый результат. Добавление другого типа шаров Поставим задачу добавить новый типа шаров, производный от исходного, но имеющий атрибут – цвет. Для того, чтобы унифицировать интерфейс шаров и использовать преимущества динамического полиморфизма, добавим в класс CBall метод SetColor, задающий цвет шара. В данном классе этот метод не будет выполнять никаких функций. Не забудьте добавить описание метода в определение класса! Создадим новый класс CColoredBall, унаследованный от CBall: В конструкторе класса будем создавать кисть, а в деструкторе уничтожать. Метод SetColor уничтожает старую кисть и создает новую. Также переопределяется метод Draw, так как его функция немного отличается. Добавим в CBallsArray реализацию метода AddColoredBall, которая будет создавать и возвращать окрашенный шар. Добавление окрашенного шара в окно будем осуществлять по нажатию правой кнопки мыши (сообщение WM_RBUTTONUP). Индивидуальные задания 1. Модифицируйте программу таким образом, чтобы при помощи клавиш ‘Q’ и ‘A’ можно было бы задавать радиус создаваемого шара (по аналогии с скоростью и направлением). Необходимо обеспечить визуализацию создаваемого радиуса, как в примере с направлением и скоростью. 2. Модифицируйте программу таким образом, чтобы при помощи клавиш 1, 2, 3, 4, 5, 6 можно было задавать цвет создаваемого по правой кнопке мыши шара (например 1,2 – r+/r-; 3,4 – g+/g-; 5,6 – b+/b-). Цвет шара должен визуализироваться как в примере с направлением и скоростью. 3. Модифицируйте программу таким образом, чтобы окрашенные шары при столкновении обменивались цветами. Неокрашенные шары цвет менять не должны. 4. Модифицируйте программу таким образом, чтобы у шаров было понятие массы. Реализуйте способы добавления шаров разной массы (например по левой кнопке мыши – легкие, по правой - тяжелые). Модифицируйте математику столкновения шаров с учетом массы. 5. Модифицируйте программу таким образом, чтобы при нажатии средней кнопки мыши (WM_MBUTTONUP) добавлялся новый тип шаров – окрашенные, окраска которых становится со временем менее насышенной, пока они не станут неокрашенными. Скорость потери окраски задается. 6. Модифицируйте программу таким образом, чтобы при нажатии средней кнопки мыши добавлялся новый тип шаров – с ограниченным временем жизни. Время жизни задается в секундах. По окончании времени жизни шар исчезает с экрана. 7. Модифицируйте программу таким образом, чтобы по нажатию средней кнопки мыши добавлялись 8 шаров, разлетающихся в разные стороны в направлениях, различающихся на 45 градусов. 8. Модифицируйте программу таким образом, чтобы по нажатию средней кнопки мыши добавлялся новый тип шаров – уничтожающиеся при столкновении. Если сталкиваются уничтожающийся и неуничтожающийся шары, то неуничтожающийся отлетает, а уничтожающийся исчезает. При столкновении двух уничтожающихся шаров исчезаю оба. Для идентификации уничтожающихся шаров необходимо придать им определенную окраску.