Мыльные пузыри в Delphi #2
В продолжение предыдущей статьи про разработку аналога заставки "Мыльные пузыри" с использованием Direct2D. Тема данной статьи - сделать пузыри движущимися. Для этих целей я использовал Windows Animation Manager, о котором так же уже рассказывалось немного ранее.
TBubbleAnimation = class(TObject) strict private FTimer : IUIAnimationTimer; FManager : IUIAnimationManager; FTransitionLibrary : IUIAnimationTransitionLibrary; public constructor Create(); procedure UpdateTimer(); property Timer : IUIAnimationTimer read FTimer; property Manager : IUIAnimationManager read FManager; property TransitionLibrary : IUIAnimationTransitionLibrary read FTransitionLibrary; end;В общем, класс является по сути контейнером, так что код его реализации весьма прост:
constructor TBubbleAnimation.Create; begin inherited; FTimer := CreateComObject(CLSID_UIAnimationTimer) as IUIAnimationTimer; FManager := CreateComObject(CLSID_UIAnimationManager) as IUIAnimationManager; FTransitionLibrary := CreateComObject(CLSID_UIAnimationTransitionLibrary) as IUIAnimationTransitionLibrary; end; procedure TBubbleAnimation.UpdateTimer; begin FManager.Update(FTimer.GetTime()); end;При инициализации создаются все нужные COM объекты - менеджер, таймер и библиотека методов анимации переменных. Для удобства введен метод UpdateTimer. Класс менеджер пузырей - TBubbleManager слегка видоизменился. Основное - он изменил предка - теперь это TInterfacedObject. Связанно данное действие с тем, что необходимо поддерживать интерфейс IUIAnimationManagerEventHandler - для перерисовки сцены, когда менеджер меняет состояние. Его метод OnManagerStatusChanged является обратным вызовом для менеджера анимации. Так что основной класс обзавелся парой новых методов:
procedure TBubbleManager.OnManagerStatusChanged(const NewStatus, PreviousStatus: TUIAnimationManagerStatus); begin if NewStatus = UIAnimationManagerBusy then UpdateWindow(); end; procedure TBubbleManager.UpdateWindow; begin InvalidateRect(FRenderTarget.GetHwnd, nil, false); end;В оригинальной заставке в начале пузыри появляются не сразу, а по очереди. Для реализации последовательного появления пузырей я ввел два новых члена класса - состояние работы FStarting, и время вылета последнего шара - FStartTime. Конечно же первое необходимо установить в true в конструкторе. А шары будут добавляться при проведении операции отрисовки Render(), а не в конструкторе.
if FStarting then AddBubble();при этом сам метод добавления шаров изменен для учета пауз между запусками.
procedure TBubbleManager.AddBubble(); const bubble_wait = 2; var t : TDateTime; begin t := now(); if SecondsBetween(t, FStartTime) < bubble_wait then exit; FStartTime := t; FBubbles.Add( TBubble.Create(FAnimation) ); FStarting := (FBubbles.Count <= MAX_BUBBLE_COUNT ) end;Т.е если пауза меньше двух секунд, то ничего не происходит. Иначе добавляется новый пузырь и запоминается время. Когда количество шаров достигает максимального, то состояние FStarting устанавливается в false. Конечно же объект анимации шаров также включен в состав менеджера и инициализируется в конструкторе, здесь же наш менеджер пузырей назначается обработчиком событий смены состояний менеджера анимации (для этого мы ввели поддержку соответствующего интерфейса выше)
FAnimation := TBubbleAnimation.Create(); FAnimation.Manager.SetManagerEventHandler(self);Чтобы закончить с описанием изменений в менеджере пузырей, скажу еще о двух вещах. Если пузырь не двигается, то его надо запустить дальше. Т.е изначально пузырь запускается из левого нижнего угла, и летит куда попало (ну или почти куда (: ), долетая до края экрана он останавливается, и вот если он вдруг остановился, то надо его подтолкнуть дальше. За это будет отвечать небольшой код в начале метода Render:
FAnimation.UpdateTimer(); for b in FBubbles do begin if not b.Moving then b.MoveNext(); end;А в конце рисования всей сцены необходимо проверить статус менеджера анимации, и если он все еще работает, то вновь обновить окно:
if FAnimation.Manager.Status = UIAnimationManagerBusy then UpdateWindow();Самые большие изменения произошли в классе-пузыре TBubble, ведь именно здесь реализуется весь функционал для отправления пузырей в полет.
TBubble = class(TObject) strict private FAnimation : TBubbleAnimation; FStoryBoard : IUIAnimationStoryboard; FMovement : IUIAnimationVariable; FSpeed : integer; FStartPos : TPoint; FAngle : real; FNextAngle : real; FColor : TColor; function getPoint() : TPoint; function isMoving():boolean; public constructor Create(Animation : TBubbleAnimation); procedure MoveNext(); property Color : TColor read FColor; property Point : TPoint read getPoint; property Moving : boolean read isMoving; end;наружу доступны несколько свойств, впрочем новое из них только одно isMoving. Цвет кстати до сих пор меняется случайным образом при долете до края экрана. MoveNext как я уже говорил отправляет шар к следующей грани. А параметром конструктора теперь является объект-контейнер для анимации, что вы могли уже заметить в процедуре добавления пузырей AddBubble. Также появились новые члены класса. FAngle & FNextAngle указывают текущий угол полета, и угол, под которым пузырь полетит, после встречи с краем экрана. FSpeed указыает скорость полета, на начальной стадии (запуска шаров) она выше. FStartPos определяет начальную точку полета - т.е координату от которой пузырь каждый раз стартует, т.е "отскакивает". Вообще если вспомнить, то для создания анимации используются три составных объекта. Во первых нужна сама переменная анимации - IUIAnimationVariable, у нас это FMovement. Далее требуется "задание" анимации, в которое добавляются когда, как долго и как переменная должна меняться - FAnimation : IUIAnimationStoryboard. Ну и третье - закон изменения данной переменной. Мы будем использовать линейное изменение. Для анимации нам требуется передвигать шар из одной точки (х0,у0) в другую (х1,у1). Для этих целей можно анимировать обе переменные х и у. Но можно пойти другим путем - для расчета координат мы можем использовать формулу расстояния между двумя этим точками. Т.е анимированная переменная в нашем случае будет представлять расстояние от точки (х0,у0) до текущей, а координаты вычислим используя синус и косинус нашего угла FAngle. Конструктор класса задает начальные значения переменных. В т.ч начальную точку, угол полета и скорость. Угол будет произвольным от 30 до 60 градусов. Так же было бы неплохо провести все вычисления в экранной системе координат, а то у меня сейчас вычисления происходят исходя из того что точка (0,0) в левом нижнем углу. Так что потом происходит переворот по Оу, что немного запутывает код. Весь алгоритм сводится к тому, что мы имеем начальную точку и угол полета. С помощью этого мы рассчитываем конечную точку - пересечение с краем экрана. После чего рассчитываем расстояние между двумя этими точками. Исходя из расстояния и скорости полета вычисляем время анимации. После чего уже создаем переменную анимации, устанавливаем ей границы, создаем StoryBoard, задаем линейную зависимость изменения и отправляем менеджер на выполнение:
FMovement := FAnimation.Manager.CreateAnimationVariable(0); FMovement.SetLowerBound(0); FMovement.SetUpperBound( Hypot(screen.Width, screen.Height) ); FStoryBoard := FAnimation.Manager.CreateStoryboard(); ltr := FAnimation.TransitionLibrary.CreateLinearTransition(duration, distance); FStoryBoard.AddTransition(FMovement, ltr); FStoryBoard.Schedule( FAnimation.Timer.GetTime() );Тут надо заметить, что для каждого передвижения пузыря от начальной к конечной точке создается новая переменная и StoryBoard. Т.е менеджер анимации владеет таким количеством StoryBoard'ов по количеству самих пузырей. Это связано с тем, что после того как мы отправили "задание анимации" на выполнение мы уже не можем его изменить, и добавить туда анимирование новой переменной/передвижения пузыря. Для расчета конечных точек столкновения с границами экрана необходимо применить простенькие знания математики и геометрии. Имея начальную точку (х0,у0) и угол FAngle, мы знаем, что соответствующее уравнение прямой будет y(x) = tan(FAngle)*(x - x0) + y0. Допустим угол лежит в первой четверти. Следовательно прямая пересекает границу экрана либо на правой либо на верхней грани. Рассчитываем значение у(х), при х = ширине экрана. Если полученное значение у меньше чем высота экрана, то пересечение с правой гранью и координата пересечения (width, y(width)). Если y(width) > height следовательно пересечение с верхней гранью. В таком случае для поиска точки надо решить уравнение - y(x) = height, и найти х при котором выполняется тождество. Код приводить не буду, ибо вроде просто, но место занимает. Кому интересно может посмотреть в приложенном коде. В общем говоря, большой сложности в реализации движения не было, хотя надо признать, что на момент написания прошлой статьи я как то не представлял как можно реализовать анимацию, ибо в предыдущие мои общения с менеджером анимаций я никогда не использовал несколько заданий анимации одновременно, и конечно же не помнил, что это возможно. Если вы вдруг решите попробовать запустить приложенный код на выполнение, то не удивляйтесь что при закрытии приложения (alt+f4) появляется Invalid pointer operation. Я честно сказать пока что не понял в чем тут причина. Для меня весьма странно, что при числе пузырей равном 15 уже наблюдаются подтормаживания. Тут может две причины, либо отрисовка, либо менеджер анимации. Третья - мои руки (: По идее с анимацией проблем не должно быть, так что возможно отрисовка. Надо попробовать отказаться от использования слоев при рисовании пузырей. На самом деле это и действительно кажется слишком сложно, ведь каждый шар рисуется отдельно. Когда фактически они все одинаковы, только цвета разные. Попробую реализовать с использованием растровой кисти в качестве маски прозрачности, по идее должно работать и в таком случае существенно повысить скорость. Так же может быть есть смысл обратить внимание на то, что мои расчеты по передвижению шаров проводятся в целых числах, хотя отрисовку можно проводить во float. Тем не менее это кажется ни при чем, поскольку когда летает 1 пузырь, то подтормаживаний нет, когда их 15, то они начинают быть заметны. Еще беспокоит меня затрата ресурсов. При 15 шарах загрузка процессора около 40%. Такое ни в какие ворота не лезет. Есть предположение что сей факт связан с тем что используется слишком много "задач анимирования". Т.е когда в одной задаче анимируются несколько переменных, как на пример в примере Grid Layout
то все нормально. А если несколько задач, то это начинает загружать процессор, в связи с тем, что чаще происходит перерисовка, ибо задачи между собой не синхронизированы. Также следует добавить изменение цвета шаров по ходу движения но это не сложно, потом сделаю. Вот так выглядят мыльные пузыри настоящий момент:
Исходный код доступен .тут (скачать), в архиве также находится скомпилированный файл, если вы хотите просто посмотреть на пузыри (: Для работы очевидно требуется Windows 7. Если д2д также доступен и в Vista то менеджер анимаций нет. Хотя с другой стороны это всего лишь COM библиотека, так что может и перенесен в Vista и другие версии. Если у вас есть идеи по поводу повышения производительности пузырей, то пишите комментарии, буду рад услышать любые замечания и предложения (:
08.11.2012 в 04:17
13.11.2012 в 20:38
А если частицы рендерить, то лучше двигаться в сторону OpenCL и Direct3D наверное.
В случае когда схожие частицы рисуются стоит обратить внимание на вот этот пример http://msdn.microsoft.com/en-us/library/dd756659%28v=vs.85%29.aspx
я что то хотел переписать его на Delphi, но руки так и не дошли.
Что касается систем частиц, то я хотел было переисать вот этот http://habrahabr.ru/post/149933/ пример на Delphi, но что то не вышло ничего. Не знаком я с Direct3D. Когда речь идет о десятках и сотнях тысяч частиц движения которых можно обрабатывать параллельно, то тут определнно стоит думать об OpenCL
07.01.2013 в 10:27