Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Создаем Tween обработчик (Actors, Unity) #3

Open
PixeyeHQ opened this issue Nov 23, 2019 · 0 comments
Open

Создаем Tween обработчик (Actors, Unity) #3

PixeyeHQ opened this issue Nov 23, 2019 · 0 comments
Labels
Actors Entity Component System Framework Programming Unity About Unity Engine

Comments

@PixeyeHQ
Copy link
Owner

PixeyeHQ commented Nov 23, 2019

Сегодня поговорим о tween-анимациях и как их реализовать в рамках ecs. Рассматривать реализацию будем через фреймворк Actors для Unity, однако данную систему можно повторить в любом языке/движке. О

Что такое tween-анимация?

Tween-анимация позволяет анимировать любое свойство предмета и с помощью интерполяции рассчитывает промежуточные значения. Повторюсь, любое свойство: от позиции до смены цвета.

Такие анимации часто незаменимы при работах с UI, например для всплывающих окошек или индикаторов. Например как в Until We Die ( игра над которой наша команда сейчас трудится )

Image from Gyazo

Описываем задачу

Нам понадобятся:

  • Функции интерполяции. Их достать легко в интернете. Есть прекрасный сайт с шпаргалками.
  • Компонент. Компонент будет хранить
    • Кол-во шагов. Это сколько раз анимация повторится. Полезно если мы хотим сделать мигающую лампочку например.
    • Время шага. Это как долго шаг будет длиться.
    • Ссылку на абстрактный класс TweenBase. Мы хотим иметь возможность анимировать любые свойства объекта а раз так нам потребуются вспомогательные классы которые будут хранить реализацию твина. Мы не будем использовать для этого делегаты, анонимные методы.
    • Процессор. Код отвечающий за обработку поступивших твинов.
    • Вспомогательные статик методы. Сахар который позволит нам быстро и красиво создавать новые tween-анимации.

Компонент

	// тип интерполяции
	public enum Tween
	{
		Linear,
		Cubicin,
		Cubicout
	}
 
	public class ComponentTween
	{
		public float dir = 1;    // вспомогательный указатель в какую сторону движется tween
		public int steps = 1;    // кол-во шагов. -1 значит анимация бесконечна
		public Tween type;       // тип интерполяции
		public float time_step;  // время на шаг
		public float time;       // текущее время
		public TweenBase source; // источник анимации
	}
 
	static partial class Component
	{
		public const string Tween = "Game.Source.ComponentTween";

		public static ref ComponentTween componentTween(in this ent entity) =>
			ref Storage<ComponentTween>.components[entity.id];
	}

	sealed class StorageComponentTween : Storage<ComponentTween>
	{
		public override ComponentTween Create() => new ComponentTween();
		// Use for cleaning components that were removed at the current frame.
		public override void Dispose(indexes disposed)
		{
			foreach (var id in disposed)
			{
				ref var component = ref components[id];
				component.steps = 1;
				component.time  = 0f;
				component.source.Dispose();
				component.source = null;
			}
		}
	}

Каждый раз когда мы хотим запустить новую анимацию мы будем создавать новую сущность и передавать ей компонент выше. Напишем простой метод расширения чтобы потом нам было легче жить. Это будет утилитарный метод и как пользователи мы вызывать его не будем.

	public static partial class TweenExtension
	{
		static void SetTween(TweenBase source, float time_step, int steps = 1, Tween tween_type = Tween.Linear)
		{
			var entity = Entity.Create();
			var cTween = entity.Set<ComponentTween>();
			cTween.time      = 0;
			cTween.dir       = 1;
			cTween.steps     = steps;
			cTween.source    = source;
			cTween.time_step = time_step;
			cTween.type      = tween_type;
		}
	}

TweenBase

Это абстрактный класс через который мы будем создавать все многообразие tween анимаций.

	public abstract class TweenBase
	{
		// задержка перед запуском твина
		public float delay;
		// метод реализации твина
                // возвращаем bool чтобы иметь возможность оборвать твин в нештатной ситуации
		public abstract bool Handle(float step);
		// чистимся если надо
		public abstract void Dispose();

		// удобно выставляем задержку
		public TweenBase setDelay(float arg)
		{
			delay = arg;
			return this;
		}
	}

ProcessorTween

Имея на руках все это безобразие мы можем приступить к обработке твинов в процессоре.

sealed class ProcessorTween : Processor<ComponentTween>, ITick
  {
    public void Tick(float delta)
    {
      foreach (ent entity in source)
      {
        var     cTween = entity.componentTween();
        ref var t      = ref cTween.time;
        ref var dir    = ref cTween.dir;
        ref var delay  = ref cTween.source.delay;
        ref var steps  = ref cTween.steps;

        // если шагов осталось 0 то убрать компонент tween, игнорируем выполнение кода на кадре.
        if (steps == 0)
        {
          entity.Remove<ComponentTween>();
          continue;
        }

        // если задержка твина больше 0 то игнорируем выполнение кода на кадре
        if ((delay -= delta) > 0.0f) continue;

        // Умножая на dir мы указываем в каком направлении сдвигать время 
        t += delta / cTween.time_step * dir;

        // Если направление 1 и время равно 1 то отнять шаг и сменить направление
        if (dir == 1)
        {
          if (t >= 1)
          {
            --steps;
            dir *= -1;
            t   =  1;
          }
        }
        // если направление -1 и время равно 0 то отнять шаг и сменить направление
        else
        {
          if (t <= 0)
          {
            --steps;
            dir *= -1;
            t   =  0;
          }
        }

        // передать интерполяцию времени t в источник твина. Если твин вернул false то удалить компонент твина.
        if (!cTween.source.Handle(math.calculateTween(cTween.type, t, 0, 1)))
          entity.Remove<ComponentTween>();
      }
    }
  }

Считаем интерполяцию

Формул гораздо больше и их нужно добавлять в этот метод.

 public static class math
  {
    public static float calculateTween(Tween type, float t, float from, float to)
    {
      switch (type)
      {
        case Tween.Linear:
          return from + (to - from) * t;
        case Tween.Cubicin:
          return from + (to - from) * t * t * t;
        case Tween.Cubicout:
        {
          var tt = t - 1;
          return from + (to - from) * (tt * tt * tt + 1);
        }
      }

      return 0;
    }
}

Создаем первую анимацию

Итак, у нас почти все готово и сейчас мы сделаем что-то вроде анимации ниже:

Image from Gyazo

Кубик ждет 1 секунду а после этого дважды мигает красным с интервалом в 0.1 сек.
Как пользователи мы используем следующее API:

 if (Input.GetKeyDown(KeyCode.P))
      {
        var spr = GameObject.Find("GameObject").GetComponent<SpriteRenderer>();
        spr.doColor(Color.red, 0.1f, 4).setDelay(1.0f);
      }

Однако метода doColor у нас пока нет. Исправим это :)
Но прежде чем нам нужно создать "тело" анимации цвета. Для этого создаем класс TweenColor и наследуем его от TweenBase

public class TweenColor : TweenBase
  {
    // Наш спрайт рендерер
    public SpriteRenderer renderer;
    // начальный цвет
    public Color val_from;
    // конечный цвет
    public Color val_to;

    // step - это интерполированное время шага от 0 до 1
    public override bool Handle(float step)
    {
      if (renderer == null) return false;
      // Формула замены цвета
      renderer.color = new Color(val_from.r + (val_to.r - val_from.r) * step, val_from.g + (val_to.g - val_from.g) * step, val_from.b + (val_to.b - val_from.b) * step, val_from.a + (val_to.a - val_from.a) * step);
      // Это тоже самое что и код выше
      // renderer.color = Color.Lerp(val_from, val_to, step);
      return true;
    }
    // Убиваем ссылку на спрайт рендерер
    public override void Dispose() => renderer = null;
  }

Теперь наконец делаем сахарный метод doColor();
Его можно будет вызвать как метод SpriteRenderer, дальше останется указать конечный цвет, время шага, кол-во шагов и тип анимации.

public static partial class TweenExtension
{
 public static TweenBase doColor(this SpriteRenderer renderer, Color to, float time_step, int steps, Tween tween_type = Tween.Linear)
    {
      var source = new TweenColor();
      source.renderer = renderer;
      source.val_to   = to;
      source.val_from = renderer.color;
      SetTween(source, time_step, steps, tween_type);
      return source;
    }
}

Заключение

 if (Input.GetKeyDown(KeyCode.P))
      {
        var spr = GameObject.Find("GameObject").GetComponent<SpriteRenderer>();
        spr.doColor(Color.red, 2f, 1);
        spr = GameObject.Find("GameObject (1)").GetComponent<SpriteRenderer>();
        spr.doColor(Color.green, 2f, 1, Tween.Cubicin);
        spr = GameObject.Find("GameObject (2)").GetComponent<SpriteRenderer>();
        spr.doColor(Color.magenta, 2f, 1, Tween.Cubicout);
      }

Image from Gyazo

У нас получилось реализовать базовый tween-обработчик. Несмотря на его простоту он покрывает нужды большинства игр и легко расширяем. До написания своего tween-обработчика я пользовался замечательной библиотекой DoTweenPro ( как и большинство Unity разработчиков )

Однако жирным плюсом ECS является возможность без лишних усилий и зависимостей внедрить новый функционал в едином с проектом архитектурном стиле.

А как вы пишите tween анимации? Делитесь своими работами и примерами : )

@PixeyeHQ PixeyeHQ added Actors Entity Component System Framework Programming Unity About Unity Engine labels Nov 23, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Actors Entity Component System Framework Programming Unity About Unity Engine
Projects
None yet
Development

No branches or pull requests

1 participant