Загадочное название в заголовке этого сообщения не должно никого настораживать. Дело в том, что недавно я решил смастерить класс-навигатор для движения объекта по заданной траектории. Казалось бы все просто - меняй координаты вдоль траектории и радуйся передвижениям тела по экрану! На деле же появилось несколько интересных моментов, которые я и решил описать здесь - поделиться так сказать. Центральная вещь класса-навигатора, конечно, состоит в движении объекта по гладкой кривой, то есть нам необходимо по заданным основным точкам траектории, восстанавливать всю сглаженную кривую, чтобы движения тела были плавными, без угловатых поворотов. Сейчас этим и займемся! |
Чуточку теории.
Сплайном как раз и зовут гладкую кривую, проходящую по заданным точкам. Обычно строится кривая определенного порядка вроде "кубического сплайна", "сплайна четвертой степени", а также известны всякие "кривые Безье", получившие популярность из-за своей простоты и хорошего результата. Также сплайны различаются по способу использования контрольных точек. В каком-то случае кривая проходит через них, в другом - контрольные точки используются для задания базового направления, но итоговая кривая через них может и не проходить (по научному это интерполирующий и аппроксимирующий сплайны):
На практике широко применяют и те, и другие... Вообще, формулы для построения сплайнов довольно "увесистые", требующие хорошей математической подготовки.
Но я сразу скажу одну приятную вещь: в glscene уже имеется класс для построения упомянутого кубического сплайна (interpolating-spline, кривая проходит через заданные точки). Есть у него ряд недостатков, но в целом съедобно. Им и воспользуемся...
Воз практики.
Дальше будет код демки, так что советую сначала скачать ее исходники к себе на компьютер и сверяться параллельно в Delphi. А вот и видео результата...
Дальше будет код демки, так что советую сначала скачать ее исходники к себе на компьютер и сверяться параллельно в Delphi. А вот и видео результата...
Приступим! Создадим основной класс для движения объектов в модуле uSplineNavigator.pas:
TSplineNavigator = classend;
и сразу прикрутим нужный нам модуль Spline.
в итоге наш класс выглядит следующим образом:
TSplineNavigator = class protected fList: TVectorList; fCubicSpline: TCubicSpline; function GetBasePointsCount: Integer; public property BasePointsCount: Integer read GetBasePointsCount; Function Add3d(const x, y, z: Single): Integer; Function Add2d(const x, y: Single): Integer; // z = 0 Function GetBasePointByIndex(const aIndex: Integer): TVector; // base points Function GetPointByUnit(const aUnit: Single): TVector; Function GetSlopeByUnit(const aUnit: Single): TVector; Procedure DeletePoints; Procedure CreateCubicSpline; Constructor Create; Destructor Destroy; override; end;
где нас по сути интересуют два метода:
Function Add2d(const x, y: Single): Integer;Function GetPointByUnit(const aUnit: Single): TVector;
Первый метод добавляет контрольную точку для построения сплайна, второй - выдает точку сплайна по указанному значению.
Объявляем fNavigator: TSplineNavigator; и вперед!
Вот все и готово, теперь этот класс нужно использовать по назначению...
Для примера я выбрал простую задачу: плыть корабликом к указанному порту на карте. Маршрут корабля должен быть сглаженным, красивым, для этого и воспользуемся TSplineNavigator'ом.
А вот и фоновая картинка, используемая нами для отображения карты местности с отмеченными портами:
Сразу оговорюсь, что для такого рода демок я решил использовать чужой арт - из существующих игр. Отвлекусь и скажу, что недавно увидел простенькую, но очень стильную и красиво оформленную игру. Потыкал, поигрался, налюбовался на красотищу - понравилось! В общем, геймплей вроде простенький, особо не затягивает, но вот сделано все грамотно: много анимации, красивые элементы, рюшечки. Оттуда я и утащил немного графики, чтобы сегодняшняя демка получилась приближенной к реальной задаче.
В общем, что мы имеем: карта, кораблик, порты. Кликаем по изображению порта на карте - наше маленькое суденышко отправляется к нему по заданному маршруту. Удерживая пробел, можно увидеть контрольные точки, по которым строится сплайн и движется корабль.
Итак, добавляем пути в каком-нибудь графическом редакторе (я делал все в FireWorks'е), потом вытаскиваем координаты точек-направляющих сплайна и забиваем в наш метод TfrmMain.StartShip():
Procedure TfrmMain.StartShip(aTradePath: Integer); begin fForwardDir := true; fTime := 0; fShiping := true; fNavigator.DeletePoints; with fNavigator do case aTradePath of 1:begin Add2d(0, 600); Add2d(100, 589); Add2d(175, 574); Add2d(275, 544); Add2d(343, 503); Add2d(403, 443); Add2d(441, 362); Add2d(443, 272); Add2d(413, 189); Add2d(371, 124); Add2d(332, 85); end; 2:begin Add2d(0, 600); Add2d(155, 559); Add2d(263, 507); Add2d(376, 471); Add2d(459, 513); Add2d(558, 573); Add2d(679, 530); Add2d(722, 447); Add2d(778, 461); end; 3:begin Add2d(0, 600); Add2d(139, 559); Add2d(277, 520); Add2d(364, 576); Add2d(486, 545); Add2d(518, 447); Add2d(463, 376); Add2d(471, 279); Add2d(574, 293); end; 4:begin Add2d(0, 600); Add2d(121, 568); Add2d(230, 517); Add2d(339, 523); Add2d(425, 582); Add2d(485, 510); Add2d(492, 415); Add2d(448, 334); Add2d(498, 253); Add2d(438, 200); Add2d(351, 174); Add2d(320, 255); end; 5:begin Add2d(0, 600); Add2d(98, 567); Add2d(205, 565); Add2d(302, 573); Add2d(345, 495); Add2d(354, 399); Add2d(427, 355); Add2d(515, 431); Add2d(488, 512); Add2d(422, 563); Add2d(345, 526); Add2d(300, 463); end; end; fNavigator.CreateCubicSpline; end;
В этом методе мы как раз сначала обнуляем все значения, чистим текущий сплайн и генерируем новый. Да, бесчисленные вывозы Add2d() делают код некрасивым, это можно обойти несколькими путями, например:
1). использовать скрипты (к примеру, Lua) и вынести все эти числа во внешние файлы
2). также, как я сделал в текущей демке для координат портов, а именно объявление константы-массива, как показано ниже
constPortsCount = 5;PortPos : array [1..PortsCount] of TVector = ((332, 82, 0, 1), (782, 466, 0, 1), (577, 294, 0, 1), (319, 258, 0, 1), (300, 464, 0, 1));
Теперь, построив сплайн для движения корбалика, мы "считаем" прошедшее время и достаем необходимые нам положение корабля и его направление движения в TfrmMain.OnDirectRender():
Pos := fNavigator.GetPointByUnit(fTime / fDuration); Dir := VectorNormalize(fNavigator.GetSlopeByUnit(fTime / fDuration)); fShip.Position.SetPoint(Pos); if not fForwardDir then fShip.Rotation := ArcTan2(Dir[1], -Dir[0]) * 180 / pi else fShip.Rotation := ArcTan2(-Dir[1], Dir[0]) * 180 / pi; fShip.Render(rci);
Также для красоты я решил добавить анимацию при наведении на эмблемку-кружок порта, куда мы хотим отправить наш корабль. Для реализации этого пришлось "захламить" код, добавив такие проверки, как:
- метод CheckPortsForSelect() - проверяет, где находится мышь и выставляет fPortSelected, fPortAnimate, fAnimationTime, fAnimateShow для обозначения подсвеченного порта, порта с анимацией (если мы увели мышь, то порт остается на некоторое время анимированным с уползающим с экрана названием, но уже не подсвеченным), время с начала анимации и тип анимации (проявляется или гаснет имя порта)
- в OnDirectRender() добавился увесистый код, создающий анимацию имени порта:
for i := 1 to PortsCount do if (fPortSelected = i) or (fPortAnimate = i)then begin fPortName.Text := PortNames[i]; fPortName.ModulateColor.SetColor(0, 0, 0, fAnimationTime / fAnimationDuration); Pos := NamesPos[i]; Pos[1] := Pos[1] + 20 * (-1 + sin(fAnimationTime / fAnimationDuration * pi /2)); fPortName.Position.SetPoint(Pos); fPortName.Render(rci); end;
Пробегусь еще раз по коду и покажу несколько приемов, которые используются в демо:
в OnCadencerProgress() имеется такой код, который управляет анимацией появления и пропадания надписи над портом (добавлю комментарии прямо сюда):
// имеется ли подсвеченный порт? if fPortAnimate > -1 then begin // анимация появления? if fAnimateShow then begin // имя появляется, увеличиваем анимационное время fAnimationTime := fAnimationTime + DeltaTime; if fAnimationTime >= fAnimationDuration then fAnimationTime := fAnimationDuration; end else // анимация пропадания имени begin // имя пропадает, уменьшаем анимационное время fAnimationTime := fAnimationTime - DeltaTime; if fAnimationTime < 0 then begin fAnimationTime := 0; fPortAnimate := -1; end; end; end;
А вот и сам метод CheckPortsForSelect(), комментарии опять помещаю внутрь:
Procedure TfrmMain.CheckPortsForSelect; var i: Integer; MPos: TPoint; dx, dy: integer; begin dx := 20; dy := 20; // узнаем координаты мыши GetCursorPos(MPos); // переводим в систему относительно viewer'а MPos := fViewer.ScreenToClient(MPos); // сначала устанавливаем подсвеченный порт в -1 fPortSelected := -1; for i := 1 to PortsCount do if (MPos.x > PortPos[i][0] - dx) and (MPos.x < PortPos[i][0] + dx) and(MPos.y > PortPos[i][1] - dy) and (MPos.y < PortPos[i][1] + dy)then begin // курсор над портом - помечаем его как подсвеченный и анимированный fPortSelected := i; fPortAnimate := i; // если до этого был подсвечен другой порт, тогда сбрасываем время if fPortSelected <> i then fAnimationTime := 0; fAnimateShow := true; break; end; // если была анимация появления, а теперь порт не подсвечен, тогда указываем // что теперь анимация пропадания имени порта if fAnimateShow and (fPortSelected = -1) then fAnimateShow := false; end;
Наверно, на этом все... что еще комментировать, я не знаю... если какие-то моменты остались неясными, пишите - постараюсь как можно подробнее ответить!
Для полной картины записал видео происходящего:
ну и сама демка с исходными кодами.
Итак, сегодня мы сделали довольно сложную демонстрационную программку, содержащую в себе несколько простых, но занимательных элементов:
- построение сплайна,
- движение объекта по кривой,
- анимационные эффекты надписей,
- реагирование эмблемок на мышь.
Да, дело оказалось нетрудным, но, надеюсь, кому-нибудь окажется полезным...
До новых встреч!
Клёво, уникальный пример!
ОтветитьУдалитьХех, я недавно написал NURBS для дельфи. Только не догнал - как сделать, чтобы аппроксимация превратилась в интерполяцию. Если интересно - могу поделиться - допилим вместе.
ОтветитьУдалитьAst (GLScene.ru)
Yarov, благодарю! надеюсь, пригодится!
ОтветитьУдалитьСтранник, здорово! насчет "допилить вместе" сейчас просто немного другим занимаюсь... времени совсем нет. огромное спасибо за доверие и предложение! надеюсь, скоро разгребу все у себя и постараюсь пригодиться в этом деле!