Delphi плюс Android? Есть идея! Tap#4

Эта заметка является продолжением предыдущих: Tap#1, Tap#2, Tap#3. И в ней я поговорю об анимации.

В моей будущей игре у персонажа будет два основных состояния. Назовём их “Открытый” и “Закрытый”. В первом состоянии внешний вид персонажа соответствует тому, который разработан в дизайнере и сохранён в переменной DesignPositions. Во втором, вид персонажа будет другой. Для робота я решил так: меняем цвет на серый, чуть-чуть уменьшаем общий размер, втягиваем ноги, руки и антенны, закрываем глаза.

Соответственно переход из состояния в состояние будет анимированным.

Делаю я следующим образом: сразу после создания фреймы, в методе SaveDesignPositions, произвожу необходимые трансформации и сохраняю положения и размеры объектов в переменную FClosedPositions. Вот так:

type
  TframeRobot = class(TNPC)
  private
    FClosedPositions: TDesignPositions;
  public
    procedure SaveDesignPositions; override;
//...
procedure TframeRobot.SaveDesignPositions;
  procedure SwitchToClose;
  const
    kBody = 9/10;
    kEye = 1/4;
    kAnt = 1/4;
    kHand = 5/6;
    kHand2 = 1/2;
    kLeg = 1/3;
  begin
    // HINT: мы смотрим роботу в лицо, поэтому левая рука - справа, правая - слева

    // уменьшаем Body
    Body.Position.X := Body.Position.X + Body.Width / 2 * (1 - kBody);
    Body.Width := Body.Width * kBody;
    Body.Position.Y := Body.Position.Y + Body.Height / 2 * (1 - kBody);
    Body.Height := Body.Height * kBody;
    // смещаем соединительные линии между телом и ногами
    LegLeftCon.Position.Y := LegLeftCon.Position.Y - LegLeftCon.Height * kBody;
    LegRightCon.Position.Y := LegRightCon.Position.Y - LegRightCon.Height * kBody;

    // уменьшаем Head
    Head.Position.X := Head.Position.X + Head.Width / 2 * (1 - kBody);
    Head.Width := Head.Width * kBody;
    Head.Position.Y := Head.Position.Y + Head.Height / 2 * (1 - kBody);
    Head.Height := Head.Height * kBody;

    // приближаем Head к Body
    HeadCenter.Position.Y := HeadCenter.Position.Y + Head.Height * (1 - kBody);

    // закрываем глазки
    EyeLeft.Position.Y := EyeLeft.Position.Y * kBody + EyeLeft.Height * kEye;
    EyeLeft.Height := EyeLeft.Height * kEye;
    EyeRight.Position.Y := EyeRight.Position.Y * kBody + EyeRight.Height * kEye;
    EyeRight.Height := EyeRight.Height * kEye;

    // уменьшаем и прячем антенны
    AntenaRight.Position.Y := AntenaRight.Position.Y * kBody + AntenaRight.Height * kBody;
    AntenaRight.Height := AntenaRight.Height * kBody * kAnt;
    AntenaRight.Position.Y := AntenaRight.Position.Y - AntenaRight.Height;

    AntenaLeft.Position.Y := AntenaLeft.Position.Y * kBody + AntenaLeft.Height * kBody;
    AntenaLeft.Height := AntenaLeft.Height * kBody * kAnt;
    AntenaLeft.Position.Y := AntenaLeft.Position.Y - AntenaLeft.Height;

    // прячем руки
    HandRightCenter.RotationAngle := 90;
    HandRightCenter.Position.X := HandRightCenter.Position.X + HandRight.Height * kHand;
    HandRightCenter.Position.Y := HandRightCenter.Position.Y + HandRight.Width * kHand2;

    HandLeftCenter.RotationAngle := -90;
    HandLeftCenter.Position.X := HandLeftCenter.Position.X - HandLeft.Height * kHand;
    HandLeftCenter.Position.Y := HandLeftCenter.Position.Y + HandLeft.Width * kHand2;

    // прячем ноги
    LegRightCenter.Position.Y := LegRightCenter.Position.Y - LegRight.Height * kLeg;
    LegLeftCenter.Position.Y := LegLeftCenter.Position.Y - LegLeft.Height * kLeg;
  end;
begin
  inherited;

  SwitchToClose;
  FClosedPositions := TDesignPositions.Create;
  SavePositionsTo(FClosedPositions);
end;

Здесь TframeRobot – это наш персонаж, про TNPC и SaveDesignPositions я писал в предыдущей заметке. Вот так выглядит наш робот до и после трансформации:

DesignPositions соответствуют картинке слева, ClosedPositions – картинке справа. Это начальное и конечное состояние в анимации. Сама анимация протекает во времени. Если взять начало времени за ноль, а конец – за единицу, то положение объекта легко рассчитать по формуле:  f(t) = a + (b – a) * t, где t – время от 0 до 1, а – начальная координата, b – конечная координата, f – искомая координата. В библиотеке FMX такая функция уже реализована и называется InterpolateSingle.

Вот так у меня выглядит процедура для изменения размеров и положений объектов робота:

procedure TframeRobot.ProcessOpenCloseAnimation(const AProgress: Single);
var
  BodyColor: TAlphaColor;
  i: Integer;
  CP, DP: TDesignPosition;
  AControl: TControl;
begin
  BodyColor := InterpolateColor($FF99CC00, TAlphaColors.Gray, AProgress);

  // цикл от 1, т.к. саму фрейму масштабировать не надо
  for i := 1 to DesignPositions.Count - 1 do
  begin
    AControl := Controls[i];
    CP := ClosedPositions.Items[i];
    DP := DesignPositions.Items[i];

    // цвет
    if (AControl is TShape) and (AControl <> EyeLeft) and (AControl <> EyeRight) then
      if TShape(AControl).Fill.Color <> TAlphaColorRec.Null then
        TShape(AControl).Fill.Color := BodyColor;

    // положение
    AControl.Position.X := InterpolateSingle(DP.Left, CP.Left, AProgress) * ScaleFactor;
    AControl.Position.Y := InterpolateSingle(DP.Top, CP.Top, AProgress) * ScaleFactor;

    // размер применяем ко всем, кроме "опорных" точек
    if Pos('Center', AControl.Name) = 0 then
    begin
      AControl.Width := InterpolateSingle(DP.Width, CP.Width, AProgress) * ScaleFactor;
      AControl.Height := InterpolateSingle(DP.Height, CP.Height, AProgress) * ScaleFactor;
    end;

    // поворот
    AControl.RotationAngle := InterpolateSingle(DP.RotationAngle, CP.RotationAngle, AProgress);
  end;
end;

(Про ScaleFactor я писал в предыдущей заметке.)

Как видите, ничего тут сложного нет.

Теперь создаём TFloatAnimation-объект, назовём его faOpenClose:

  object faOpenClose: TFloatAnimation
    Duration = 0.300000000000000000
    OnProcess = faOpenCloseProcess
    PropertyName = 'OpenCloseValue'
  end

Далее у фреймы:

type
  TframeRobot = class(TNPC)
  ..
    procedure faOpenCloseProcess(Sender: TObject);
  private
    FOpenCloseValue: Single;
    FClosed: Boolean;
  ..
  public
    property OpenCloseValue: Single read FOpenCloseValue write FOpenCloseValue;
  ..
procedure TframeRobot.faOpenCloseProcess(Sender: TObject);
begin
  ProcessOpenCloseAnimation(FOpenCloseValue);
end;

И осталось запустить анимацию в прямом:

procedure TframeRobot.CloseWithAnimation;
begin
  FClosed := True;
  faOpenClose.Stop;
  faOpenClose.StartValue := 0;
  faOpenClose.StopValue := 1;
  faOpenClose.Start;
end;

, либо обратном направлении:

procedure TframeRobot.OpenWithAnimation;
begin
  FClosed := False;
  faOpenClose.Stop;
  faOpenClose.StartValue := 1;
  faOpenClose.StopValue := 0;
  faOpenClose.Start;
end;

 

Ну и теперь, мы можем реализовать метод ScaleControls, который в прошлой заметке остался не реализованным. Выглядит он у меня так:

procedure TframeRobot.ScaleControls;
  procedure ScalePositions;
  begin
    // устанавливаем расположение контролов в зависимости от текущего состояния
    // (процедура анимации учтёт ScaleFactor)
    if FClosed then
      ProcessOpenCloseAnimation(1)
    else
      ProcessOpenCloseAnimation(0)
  end;

  procedure ScaleStrokeThickness;
  var
    i: Integer;
  begin
    for i := 1 to Length(Controls) - 1 do
      if Controls[i] is TShape then
        if TShape(Controls[i]).Stroke.Kind = TBrushKind.bkSolid then
          TShape(Controls[i]).Stroke.Thickness := 12 * ScaleFactor;
  end;

  procedure RecalcBodyRadius;
  begin
    Body.XRadius := (LegLeft.LocalRect.Width - LegLeft.Stroke.Thickness) / 2;
    Body.YRadius := Body.XRadius;
  end;

  procedure RecalcAntenaRadius;
  begin
    AntenaLeft.XRadius := (AntenaLeft.Width - AntenaLeft.Stroke.Thickness) / 2;
    AntenaLeft.YRadius := AntenaLeft.XRadius;
    AntenaRight.XRadius := AntenaLeft.XRadius;
    AntenaRight.YRadius := AntenaLeft.XRadius;
  end;

begin
  ScalePositions;
  ScaleStrokeThickness;
  RecalcBodyRadius;
  RecalcAntenaRadius;
end;

 

Заметка получилась больше, чем я предполагал, поэтому закругляюсь. Напоследок скажу, что кроме анимации по смене состояния, у меня будет ещё как минимум одна анимация. Анимация-приветствие. Всё вместе выглядит так:

TapTap

Читать на сайте автора.