Как стать автором
Обновить

Как построить управление анимациями персонажа в Unigine

Время на прочтение5 мин
Количество просмотров1.9K

Всем привет, не так давно я перешел на Unigine, и хотел бы поделиться опытом создания системы анимаций персонажа, так как релевантных материалов по теме как-то не нашел, а примеры разработчиков мне показались чересчур сложными. Все примеры будут написаны на C#.

Статья расчитана на людей, которые совсем не знакомы с движком, поэтому распишу поэтапно, как и что нужно делать. Для начала создадим сцену и создадим папку под модели в Asset Browser:

Далее в нужной папке нажимаем ПКМ и в выпадающем меню нажимаем Import New Asset, указываем путь, появляется меню импорта. Оно довольно большое, но по умолчанию все чекбоксы выставлены оптимально, поэтому не вижу смысла на нем останавливаться. Итак, мы импортировали основное тело с риггом и две анимации, которые будем чередовать - idle и walk. Перетащим основной скелет на сцену и посмотрим, что получится.

Отлично, у нас есть меш на сцене и есть пара настроек. В окне Parameters видим тип объекта - ObjectMeshSkinned, а ниже - его данные, такие как mesh, preview animation и прочее. Прямо здесь можно установить анимацию и проиграть ее, для этого в preview animation нажимаем кнопку папки, выбираем модель с анимацией. Затем нажимаем Play.

Но скорее всего вам нужно контролировать анимации из скрипта, поэтому давайте поставим галочку Controlled в параметрах меша, затем создадим компонент AnimController и посмотрим, как можно изменять состояние через код. Для этого в окне Asset Browser нужно выбрать папку, нажать там ПКМ -> Create Code -> C# Component. Открываем появившийся файл в любимом текстовом редакторе или IDE и наблюдаем следующее:

Component(PropertyGuid = "uid компонента")]
public class AnimController : Component
{
  private void Init() {

  }
  private void Update() {
  
  }
}

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

public ObjectMeshSkinned meshSkinned = null;
[ParameterFile(Filter = ".anim")]
public string idleAnimation = "";
[ParameterFile(Filter = ".anim")]
public string walkAnimation = "";

Теперь повесим этот скрипт на родительскую ноду меша (создается автоматически при перетаскивании модели на сцену), хотя можно и на сам меш - непринципиально. В подменю Node Components And Properties создаем новый компонент и перетаскиваем наш скрипт, должна появиться такая картина:

Здесь нужно перетащить меш на место Mesh Skinned и выбрать нужные анимации, думаю, с этим проблем возникнуть не должно.

Возвращаемся к скрипту. Для начала нужно забиндить слои анимаций. В Unigine нет визуального создания animation state machine, поэтому будем создавать его сами. Для начала зарегистрируем наши idle и walk:

private void Init() {
  // указываем количество слоёв
  meshSkinned.NumLayers = 2;
  // устанавливаем на слои анимации
  meshSkinned.SetAnimation(0, idleAnimation);
  meshSkinned.SetAnimation(1, walkAnimation);
  // включаем слои и устанавливаем им blending веса
  meshSkinned.SetLayer(0, true, 1f);
  meshSkinned.SetLayer(1, true, 0f);
}

Таким образом, наш персонаж научился переключаться на анимацию. Но анимацию нужно еще и проигрывать, а так как в Unigine нет аналогов StartCoroutine или Invoke из Unity (UPD: они есть, но к анимациям их применить не получится, кадр анимации будет обновляться только если устанавливать его в Update(). А так, в движке работает стандартный async Task из шарпа), то напишем все руками в Update:

private float currentTime = 0f;
private float animationSpeed = 30f;

private void Update() {
  // устанавливаем для каждого слоя текущий момент
  meshSkinned.SetFrame(0, currentTime * animationSpeed);
  meshSkinned.SetFrame(1, currentTime * animationSpeed);
  // добавляем время между прошлым вызовом Update
  currentTime += Game.IFps;
}

Отлично, теперь анимация будет проигрываться, неплохо бы заиметь функционал, чтобы поменять одну на другую?

void SetWalk(bool walk) {
  if (walk) {
    meshSkinned.SetLayerWeight(1, 1);
    meshSkinned.SetLayerWeight(0, 0);
  }
  else {
    meshSkinned.SetLayerWeight(1, 0);
    meshSkinned.SetLayerWeight(0, 1);
  }
}

Вообще говоря, мы могли бы и не использовать веса, а занимать нулевой слой нужной нам анимацией, тогда необязательно было в Init() вызывать SetLayer(), а переключение выглядело бы так:

void SetWalk(bool walk) {
  if (walk) {
    meshSkinned.SetAnimation(0, walkAnimation);
  }
  else {
    meshSkinned.SetAnimation(0, idleAnimation);
  }
}

Но раз уж мы выбрали реализацию с весами, давайте добавим немного плавности. Я использовал следующий подход:

public delegate void AnimTask(ObjectMeshSkinned ob, float now);

private AnimTask blendingTask = null;
private float now = 0f;

public void SetBlendingTask(AnimTask task, float timeBlend) {
    blendingTask = task;
    now = timeBlend;
}

private void SetToWalk(ObjectMeshSkinned meshSkinned, float now) {
    meshSkinned.SetLayerWeight(1, 1f - now);
    meshSkinned.SetLayerWeight(0, now);
}

private void SetToStand(ObjectMeshSkinned meshSkinned, float now) {
    meshSkinned.SetLayerWeight(0, 1f - now);
    meshSkinned.SetLayerWeight(1, now);
}

Тогда нужно и SetWalk() чуть подправить:

public void SetWalk(bool walk) {
  if (walk) {
    SetBlendingTask(SetToWalk, 0.4f);
  }
  else {
    SetBlendingTask(SetToStand, 0.7f);
  }
}

А в Update добавить изменение значения переменной now и вызов поставленной задачи:

private void Update() {
  /* ... */

  if (now > 0f) {
      now -= Game.IFps;
      if (now <= 0f) { now = 0f; }

      blendingTask(meshSkinned, now);
  }
}

Итак, что мы имеем: у нас есть указатель на функцию, которая изменяет веса слоев, этот указатель устанавливается через функцию SetWalk и вызывается каждый кадр, пока время перехода не станет равно нулю. Время перехода убавляется временем обработки кадра.

В итоге, получаем что-то подобное:

[Component(PropertyGuid = "uid компонента")]
public class AnimController : Component
{
    public delegate void AnimTask(ObjectMeshSkinned ob, float now);

	public ObjectMeshSkinned meshSkinned = null;
    private float animationSpeed = 30f;
    private float currentTime = 0.0f;
    [ParameterFile(Filter = ".anim")]
	public string idleAnimation = "";
    [ParameterFile(Filter = ".anim")]
	public string walkAnimation = "";

    private AnimTask blendingTask = null;
    private float now = 0f;

    private void Init() {
        // указываем количество слоёв
        meshSkinned.NumLayers = 2;
        // устанавливаем на слои анимации
        meshSkinned.SetAnimation(0, idleAnimation);
        meshSkinned.SetAnimation(1, walkAnimation);
        // включаем слои и устанавливаем им blending веса
        meshSkinned.SetLayer(0, true, 1f);
        meshSkinned.SetLayer(1, true, 0f);
    }

    private void Update() {
        meshSkinned.SetFrame(0, currentTime * animationSpeed);
        meshSkinned.SetFrame(1, currentTime * animationSpeed);
        currentTime += Game.IFps;

        if (now > 0f) {
            now -= Game.IFps;
            if (now <= 0f) { now = 0f; }

            blendingTask(meshSkinned, now);
        }
    }

    public void SetWalk(bool walk) {
        if (walk) {
            SetBlendingTask(SetToWalk, 0.4f);
        }
        else {
            SetBlendingTask(SetToStand, 0.7f);
        }
    }
    
    public void SetBlendingTask(AnimTask task, float timeBlend) {
        blendingTask = task;
        now = timeBlend;
    }
    
    private void SetToWalk(ObjectMeshSkinned meshSkinned, float now) {
        meshSkinned.SetLayerWeight(1, 1f - now);
        meshSkinned.SetLayerWeight(0, now);
    }
    
    private void SetToStand(ObjectMeshSkinned meshSkinned, float now) {
        meshSkinned.SetLayerWeight(0, 1f - now);
        meshSkinned.SetLayerWeight(1, now);
    }
}

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

В заключение, хотелось бы пару слов сказать про сам движок. Он мне понравился визуальными возможностями, неплохой производительностью и более-менее понятной документацией, однако, поначалу она была очень неудобной в использовании. Примеры разработчиков часто перегружены, чего только стоит 3д платформер, где компонент управления персонажем занимает почти 700 строк и содержит в своем названии "Simplified", что похоже на злую иронию. В силу понятно каких событий выбор лицензионного ПО ограничен, а Unigine является российской разработкой, поэтому я надеюсь на развитие как самого движка (а он действительно развивается, судя по появляющимся фичам), так и его коммьюнити, ну и надеюсь, что этой статьей я сделал вклад в это самое коммьюнити.

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

листаем SimplifiedPlayerController.cs в 4 утра
листаем SimplifiedPlayerController.cs в 4 утра

Теги:
Хабы:
Всего голосов 1: ↑1 и ↓0+1
Комментарии4

Публикации

Истории

Работа

Ближайшие события