12 апреля 2011

Tween-инг в Delphi, первые шаги

Вот и пришло время выложить демонстрацию tween'инга в Delphi! Если кто забыл, то я как раз недавно озаботился вопросом плавной анимации движения/поворотов/альфы и всего прочего. И даже начал кое-что делать в этом направлении. Конечно, пока работа над этим не закончена, но уже есть что показать. Поэтому сегодня постараюсь описать текущий принцип работы tween-класса. Надеюсь появятся вопросы и предложения... а все знают, что лучше дополнять код в процессе работы, а не переписывать потом все с нуля. Вперед, друзья!

Начнем с основного элемента tween-инга, класса TBaseTweenItem, публичная часть которого выглядит так:

  TBaseTweenItem = class
  ...
  public
    property Done: Boolean read fDone;
    Procedure Play; virtual;
    Procedure Pause; virtual;
    Procedure SetPause(const aPause: Boolean); virtual;
    Procedure Update(const aDeltaTime: Single); virtual;
    Constructor Create(aDuration: Single; aPauseOnStart: Single);
  end;

Это просто класс, хранящий время анимации, обновляющийся методом Update() и способный становиться на паузу. Ничего особо сложного и выдающегося, за исключением того, что все остальные элементы tween'инга будут наследоваться от этого, самого простого и базового класса.

Вообще, контролирование каждого tween-объекта довольно неприятное дело (нужно пробежаться по списку и каждому вызвать Update(), а также нужно где-то хранить и сам список), поэтому я решил взвалить всю эту работу на менеджер TTweener, который хранит в себе список всех tween-элементов. Сам класс довольно простой, описание также довольно прозрачно:

  TTweener = class
  protected
    fTweenItems: TList;
    fEasingFunctions: TBaseEasingFunctions;
    Function GetTweenCount: Integer;
  public
    property TweenCount: Integer read GetTweenCount;

    Function GetItemByIndex(const aIndex: integer): TBaseTweenItem;
    Procedure FreeByIndex(const aIndex: integer);
    Procedure FreeAll;
    Function AddTweenItem(aTweenItem: TBaseTweenItem): Integer; virtual;

    Procedure AddTweenPSingle(aVariable: PSingle;    aTweenStyle: TTweenStyle; const aStartValue, aFinishValue, aDuration: Single; const aPauseOnStart: Single = 0);
    Procedure AddTweenPVector(aVariable: PVector;    aTweenStyle: TTweenStyle; const aStartValue, aFinishValue: TVector; aDuration: Single; const aPauseOnStart: Single = 0);
    Procedure AddTweenSingle (aObject: TTweenObject; aSetValue: TSetSingle; aTweenStyle: TTweenStyle; const aStartValue, aFinishValue, aDuration: Single; const aPauseOnStart: Single = 0);

    Procedure Update(const aDeltaTime: Single);

    Constructor Create;
    Destructor Destroy; override;
  end;

Теперь по поводу самого tween'инга.
Можно увидеть, что у TTweener есть два метода, назначение которых - создание tween'ера, который будет менять значение Single-значений:

    Procedure AddTweenPSingle(aVariable: PSingle;    aTweenStyle: TTweenStyle; const aStartValue, aFinishValue, aDuration: Single; const aPauseOnStart: Single = 0);
    Procedure AddTweenSingle (aObject: TTweenObject; aSetValue: TSetSingle; aTweenStyle: TTweenStyle; const aStartValue, aFinishValue, aDuration: Single; const aPauseOnStart: Single = 0);

Разница между ними в том, что первый метод создает элемент tween'инга, который меняет значение переменной по определенному адресу (адрес переменной указывается аргументом aVariable: PSingle). Второй метод общается с основной программой при помощи события aSetValue: TSetSingle, что больше подойдет для изменения свойств объектов (так как прямая запись по адресу в таком случае будет невозможна).

Сам я планирую описывать такие методы добавления tween'инга парами: с передачей адреса или же с передачей обработчика изменений.
На очереди векторные типы (это единственная отсылка к GlScene), integer-значения, а также под вопросом string-типы (вроде эффекта набивания текста).

Разберем способ добавления tween-а для численного single-типа, с передачей адреса переменной. Напомню метод добавления:

    Procedure AddTweenPSingle(aVariable: PSingle;    aTweenStyle: TTweenStyle; const aStartValue, aFinishValue, aDuration: Single; const aPauseOnStart: Single = 0);

где
  • aVariable - адрес переменной, значение которой будет изменяться
  • aTweenStyle - способ tween'инга
  • aStartValue - стартовое значение
  • aFinishValue - конечное значение
  • aDuration - длительность tween'инга
  • aPauseOnStart - пауза при старте, то есть можно отсрочить начало изменений
После добавления необходимых элементов, остается только регулярно вызывать Update(). Замечу, что сейчас, как только tween дойдет до конца, он сразу удаляется из списка, освобождая память. Не знаю, стоит ли эту возможность выносить в отдельное свойство TBaseTweenItem'а или лучше вставить в качестве аргумента при создании объекта tween'инга. Время покажет :)

Демонстрация применения.
Как и обещал, сегодня покажем tween в работе. Видео можно посмотреть здесь, исходники + ехе качаем отсюда. На скриншоте результат, конечно, не виден (как можно анимацию показать на статической картинке?), но все же приведу его для ознакомления:


Как уже стало ясно, с самого начала объявляем основной менеджер-объект fTweener: TTweener, а также сразу объявляем четыре вектора, которые будут хранить позиции четырех кнопок Pos: array[1..4] of TVector;
В методе StartTween(aType: Integer) стартуем анимацию, добавляя четыре tween-элемента для каждой кнопки. Аргумент aType задает, в какую позицию стремятся переместиться кнопки: влево, наверх, вправо или же вниз нашего окошка.
Остается только создать наш менеджер и запустить первоначальную анимацию:

  fTweener := TTweener.Create;
  StartTween(1);
А дальше только отслеживать нажатия клавиши мыши и добавлять новые tween-элементы:

if IsKeyDown(vk_LButton) and not MousePressed then
  begin
    i := GetHudSpriteAtPos(mPos.x, mPos.y);
    if i <> -1 then
      StartTween(i);
  end;

Ну и не забывает обновлять наш менеджер, который управляет всеми текущими анимациями, а также применять новые значения, которые постоянно меняют наши замечательные tween'еры:

  fTweener.Update(DeltaTime);
  for i := 1 to 4 do
    fHudSprites[i].position.SetPoint(Pos[i]);

То есть после обновления, вектора Pos[] уже хранят новые, измененные значения. Удобство, ради которого немного жертвуем безопасностью (мало ли неверный адрес переменной передадим при создании tween'инга?!)
А вот и видео результата:


Вообще, такого рода tween'инг больше применим в сфере gui, так как выползающие кнопочки, панельки и прочее - очень радуют игроков!
На этом, пожалуй, все... следите за обновлениями!

Сообщения, схожие по тематике:

6 коммент.:

  1. Спасибо за очень классные и познавательные статьи.
    p.s. О новой статье знал заранее, так как подписан на твой youtube-канал, где видео уже несколько дней как залито =)

    ОтветитьУдалить
  2. А нельзя ли переименовать этот пост (предположительно, убрав апостроф, заменив на дефис)?

    А то delphifeeds.ru с ума по нему сходит.

    ОтветитьУдалить
  3. Красиво выглядит. =)
    Однако, присоединюсь к просьбе Gunsmoker-a.

    ОтветитьУдалить
  4. gltrinix, благодарю! твининг вообще картинку более опрятной делает. готовлю плацдарм к более сложным демкам... надеюсь, сил хватит, чтобы их в скором времени написать и выложить!

    GunSmoker, Aleksey Timohin, ага, переименовал! благодарю за подсказку! оставил только дефис и запятую... надеюсь теперь все будет хорошо!

    ОтветитьУдалить
  5. прости, не в тему. просто посмотри:
    http://www.youtube.com/watch?v=8tEf1_EKR8c&feature=player_embedded#at=26

    ОтветитьУдалить
  6. Aero, выглядит очень интересно! правда гамма очень мрачновата(( получается, своим шлейфом герой уничтожает противников, так?

    ОтветитьУдалить