Урок 4: Игровой цикл. Часть 3 (Процедурная анимация в Silverlight’e и игровой цикл)

Собственно если вы уже понимаете анимацию Silverlight’a на базе xaml, то процедурная анимация точно для вас не составит труда.

Как случай чистой xaml анимации, так и случай чистой процедурной анимации на C# в реальной жизни по отдельности очень редко встречается. Но для полноты эксперимента мы обязаны их рассмотреть. Так что давайте для начала попробуем создать таймер. Вот что у нас получилось в файле page.xaml.cs:

   1:   Page : Canvas
   2:      {
   3:          Storyboard animation_timer; // объявляем экземпляр таймера Storyboard
   4:          public Page()
   5:          {
   6:              InitializeComponent();
   7:  
   8:              animation_timer = new Storyboard(); // создаем наш таймер
   9:              this.Resources.Add("animation_timer", animation_timer); // добавляем его в ресурсы страницы
  10:              animation_timer.Begin(); // собственно запускаем его
  11:          }
  12:      }

Понадобилось всего четыре строчки для того чтобы создать таймер и запустить его. Хотелось обратить внимание на одну тонкость – вы обязаны добавить в ресурсы страницы или контрола свой таймер (this.Resources.Add(«animation_timer», animation_timer)), иначе ничего не будет работать.

Таймер это хорошо, но хотелось бы получить именно процедурную анимацию. Для этого по аналогии с прошлой частью урока мы на страницу добавим квадрат с группой трансформации и посмотрим, что мы с ним сможем сделать:

   1:      <Canvas
   2:          x:Class="SilverlightApplication8.Page"
   3:          xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
   4:          xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
   5:          Width="400" Height="300"
   6:          Background="Black">
   7:          <Rectangle Height="100" Width="100" Fill="Red" x:Name="rectangle" Canvas.Top="100" Canvas.Left="100">
   8:              <Rectangle.RenderTransform>
   9:                  <TransformGroup>
  10:                      <RotateTransform x:Name="rectangle_rotate"/>
  11:                      <TranslateTransform x:Name="rectangle_move"/>
  12:                  </TransformGroup>
  13:              </Rectangle.RenderTransform>
  14:          </Rectangle>
  15:      </Canvas>

Чтобы у вас не возникло никакой иллюзии – это полный xaml файл, просто я оставил самое необходимое, чтобы ничего не отвлекало внимание.

Итак, давайте же добавим процедурную анимацию красного квадрата, для простоты сначала From/To типа. Для этого нам понадобится создать объект класса DoubleAnimation и назначить ему и таймеру несколько необходимых свойств. В связи «с непрозрачностью» приведем полный листинг класса страницы:

   1:      public partial class Page : Canvas
   2:      {
   3:          Storyboard animation_timer;
   4:          DoubleAnimation double_animation;
   5:  
   6:          public Page()
   7:          {
   8:              InitializeComponent();
   9:  
  10:              animation_timer = new Storyboard(); // создаем экземпляр таймера
  11:              double_animation = new DoubleAnimation(); // создаем экземпляр класса анимации
  12:  
  13:              this.Resources.Add("animation_timer", animation_timer); // добавляем наш таймер в ресурсы страницы
  14:              animation_timer.Children.Add(double_animation); // в качестве потомка к таймеру добавляем нашу анимацию
  15:  
  16:              Duration duration = new Duration(TimeSpan.FromSeconds(2)); // создаем продолжительность таймера и анимации
  17:              // по большому счёту они могут быть разными это видно было в предидущей части урока
  18:              animation_timer.Duration = duration;
  19:              // выставляем параметры анимации
  20:              double_animation.Duration = duration;
  21:              double_animation.From = 0;
  22:              double_animation.To = 45;
  23:  
  24:              // самая непрозрачная часть продцедурной анимации
  25:              // добавление таргета и свойства изменения в Beta 2
  26:              Storyboard.SetTarget(double_animation, rectangle_rotate);
  27:              Storyboard.SetTargetProperty(double_animation, new PropertyPath("rectangle_rotate.Angle"));
  28:  
  29:              // наконец-то запускаем анимацию
  30:              animation_timer.Begin();
  31:          }
  32:      }

Обратите внимание, я в комментариях отметил, что изменилось в Бета 2. Появилось понятие PropertyPath.

Сами видите, что процедурная анимация при таком подходе по необходимым шагам в точности повторяет xaml основаную анимацию.

Честно могу сказать – я собирался еще добавить и листинг реализации Key-Frame анимации на три кадра с разными вариантами интерполяции свойства, но текст тогда не только вырос в четыре раза, но еще и окончательно утратил свою наглядность. Поэтому приводить его не буду, всем желающим, при необходимости, просто рекомендую поискать исходники в интеренете. Их там не сильно много, но есть.

Глядя на листинг с процедурной анимацией, может стать совсем грустно – во-первых, не совсем понятно как организовать игровой цикл при таком подходе, во-вторых, кому как, а мне хотелось бы чего ни будь более простого для организации анимации.

К счастью есть еще одна возможность организовать анимацию – событие Storyboard.Completed. Помните наш первый листинг с этой части урока? Давайте в него добавим пару строчек (приведу класс страницы полностью):

   1:      public partial class Page : Canvas
   2:      {
   3:          Storyboard animation_timer; // объявляем экземпляр таймера Storyboard
   4:  
   5:          public Page()
   6:          {
   7:              InitializeComponent();
   8:  
   9:              animation_timer = new Storyboard(); // создаем наш таймер
  10:              this.Resources.Add("animation_timer", animation_timer); // добавляем его в ресурсы страницы
  11:              // добавляем функцию по окончании таймера
  12:              animation_timer.Completed += new EventHandler(animation_timer_Completed);
  13:              animation_timer.Begin();  // собственно запускаем таймер 
  14:          }
  15:  
  16:          void animation_timer_Completed(object sender, EventArgs e)
  17:          {
  18:              // сюда добавляем то что надо сделать в цикле анимации
  19:  
  20:              animation_timer.Begin();
  21:          }
  22:  
  23:      }

Всего-то и понадобилось обработать событие срабатывания таймера и запустить его снова. Ничего не напоминает? По-моему, у нас получился простейший игровой цикл. Добавим в него простейшую анимацию:

   1:          void animation_timer_Completed(object sender, EventArgs e)
   2:          {
   3:              // сюда добавляем то что надо сделать в цикле анимации
   4:              rectangle_rotate.Angle++; // вращаем квадрат с центром (0,0) - по умолчанию
   5:              rectangle_move.X++; rectangle_move.Y = rectangle_move.Y - 2;// просто двигаем квадрат для примера
   6:  
   7:              animation_timer.Begin();
   8:          }

Думаю все всем понятно. В таком виде намного привычнее и легче воспринимается. Хотите, работайте через имена на прямую, хотите забиндите свойства на класс данных.

Уже вполне имеем жизнеспособный игровой цикл. Остальное зависит только от Вас.

Напоследок, приведу полный листинг класса игрового цикла, которым сам пользуюсь. За основу взят код вот отсюда (За что я им премного благодарен). GameLoop.cs:

   1:  using System;
   2:  using System.Windows;
   3:  using System.Windows.Media.Animation;
   4:  
   5:  namespace GameLoop
   6:  {
   7:      public struct GameLoopUpdateEventArgs
   8:      {
   9:          public GameLoopUpdateEventArgs(TimeSpan elapsed)
  10:          {
  11:              Elapsed = elapsed;
  12:          }
  13:          public TimeSpan Elapsed;
  14:      }
  15:  
  16:      public class GameLoop
  17:      {
  18:          private bool stopped = true;
  19:          private Storyboard gameLoop = new Storyboard();
  20:          private DateTime now;
  21:          private TimeSpan elapsed;
  22:          private DateTime lastTick;
  23:  
  24:          public delegate void UpdateHandler(object sender, GameLoopUpdateEventArgs e);
  25:          public event UpdateHandler Update;
  26:  
  27:          public GameLoop(FrameworkElement parent) : this(parent, 0)
  28:          {
  29:          }
  30:  
  31:          public GameLoop(FrameworkElement parent, double milliseconds)
  32:          {
  33:              gameLoop.Duration = TimeSpan.FromMilliseconds(milliseconds);
  34:              gameLoop.SetValue(FrameworkElement.NameProperty, "gameloop");
  35:              parent.Resources.Add("gameloop", gameLoop);
  36:              gameLoop.Completed += new EventHandler(gameLoop_Completed);
  37:          }
  38:  
  39:          void gameLoop_Completed(object sender, EventArgs e)
  40:          {
  41:              if (stopped) return;
  42:  
  43:              now = DateTime.Now;
  44:              elapsed = now - lastTick;
  45:              lastTick = now;
  46:  
  47:              if (Update != null) Update(this, new GameLoopUpdateEventArgs(elapsed));
  48:  
  49:              (sender as Storyboard).Begin();
  50:          }
  51:  
  52:          public virtual void Start()
  53:          {
  54:              lastTick = DateTime.Now;
  55:              stopped = false;
  56:  
  57:              gameLoop.Begin();
  58:          }
  59:  
  60:          public virtual void Stop()
  61:          {
  62:              stopped = true;
  63:          }
  64:      }
  65:  }

А вот простейший пример его использования:

   1:      public partial class Page : Canvas
   2:      {
   3:          GameLoop gameLoop;
   4:  
   5:          public Page()
   6:          {
   7:              InitializeComponent();
   8:  
   9:              gameLoop = new GameLoop(this);
  10:              gameLoop.Update += new GameLoop.UpdateHandler(gameLoop_Update);
  11:              gameLoop.Start();
  12:  
  13:          }
  14:  
  15:          void gameLoop_Update(object sender, GameLoopUpdateEventArgs e)
  16:          {
  17:              // собственно код нашего игрового цикла
  18:              rectangle_move.X += moveSpeed * elapsed.TotalSeconds ;
  19:          }
  20:  
  21:      }

Вроде бы все. Об основах анимации и игровом цикле, что знал – рассказал.

Если Вы внимательно читали, то поймете, почему ничего не было сказано о битмап анимации. Потому что Silverlight её не поддерживает, т.к. отсутствует класс анимации необходимого свойства.

Как работать с изображениями и что можно придумать в нашем случае тема другого урока.

Комментариев пока нет

Ответить