Пишем Third Person Controller на MonoGame. Часть II
Введение
Это вторая часть серии туториалов, где мы реализуем Third Person Controller на MonoGame.
Первая часть доступна тут:
В этой части мы заменим капсулу персонажа на анимированную модель. И прицепим меч на спину:
Для понимания материала необходимо иметь представление о том, что такое gltf/glb и как работает скелетная анимация. В Интернете полно информации на эту тему. Например, статья на learnopengl:
Объединение fbx анимаций в одну модель gltf(необязательная глава)
Эта глава необязательна для туториала. Поскольку в нём будет предоставлена модель gltf сразу со всем нужными анимациями.
Но если вы сами захотите создать свою анимированную модель на базе mixamo, то вам будет полезно посмотреть это видео, где вначале необходимые анимации скачиваются. А потом объединяются в blendere и экспортятся в glb(бинарная версия gltf):
Важно держать в уме следующие моменты:
- Первую анимацию из Mixamo (например, «Idle») нужно скачать с параметром «With Skin», все остальные — с параметром «Without Skin»
- Убедитесь, что установлен флажок «In Place». У некоторых анимаций этот флажок отсутствует. Это можно исправить в Blender с помощью дополнения In Placer
- В Blender сначала импортируйте FBX-файл, скачанный с параметром «With Skin» (то есть базовую модель), а затем все остальные
- Не забудьте удалить все арматуры (Armatures), кроме арматуры базовой модели, после нажатия кнопок «Push Down Action»
Стартовая точка
Мы продолжим с того же самого места, на котором остановились в конце прошлой части.
Если у вас не сохранилось исходного кода того туториала, то вы можете взять его здесь:
Небольшие изменения
Для начала мы внесём небольшие QoL изменения в исходный код:
- Включим DefaultLights в BasicEffect
- Разобьём метод Update на части
- Вынесем часть кода DrawMesh в отдельный метод
Включаем DefaultLights
Удалите код, инициализирующий DirectionalLight0 в _basicEffect. И добавьте вместо него(т.е. кода) такую строку:
Это сделает сцену более освещённой:
Рефактор Update
Добавим 3 метода: ProcessMouse, ProcessKeyboard и UpdateJump
И перенесём в них соответствующий код:
Теперь код стал более простым и читаемым.
Рефактор DrawMesh
Вынесем рисование DrMeshPart в отдельный метод:
Промежуточный итог
Результат нашего рефакторинга можно посмотреть тут:
Заменяем капсулу на модель
Скачайте и разархивируйте в папку с проектом:
Архив состоит только из gltf модели персонажа.
Перейдём к коду.
Удаляем поле _meshHero и весь код, который с ним работает. Вместо него добавляем следующие поля:
SkinnedEffect - это аналог BasicEffect, только умеющий в скелетную анимацию.
_textureWhite - белая текстура единичного размера. Она нам нужна, поскольку у нашей модели нет текстур.
Добавляем инициализацию новых полей в LoadContent:
Если с инициализацией _skinnedEffect и _textureWhite всё более ни менее понятно.
То про загрузку модели стоит сказать пару слов. Здесь мы применили два класса DrModel и DrModelInstance.
DrModel содержит информацию о модели: меши, иерархия костей, анимации, материалы и т.д. Фактически DrModel является immutable.
DrModelInstance является экземпляром DrModel. И содержит такую изменяющуюся информацию, как трансформации костей и т.д.
Поэтому каждую DrModel мы будем оборачивать в DrModelInstance. Хотя в этой серии туториалов на одну DrModel будет приходится один DrModelInstance. В реальных приложениях, на одну модель может приходиться сколько угодно экземпляров.
Метод DrawModel
Добавим следующий метод:
В этом коде наиболее интересна проверка Part.Skin на null и выбор эффекта для рисования на основе результата.
Для понимания этого кода нужно иметь представление о том, как устроена Gltf-модель.
В ней с каждым мешем может быть связан скин. Скин - это коллекция костей. Вызов model.GetSkinTransforms возвращает массив матриц трансформации, соответствующий этой коллекции. Который мы и передаём в SkinnedEffect.
Если с мешем никакой скин не связан, то пользуемся обычным BasicEffect.
Обратите так же внимание, что при наличии скина, мы передаём в параметр World только мировую матрицу трансформации модели(world).
А при его отсутствии, мы дополнительно умножаем её на матрицу трансформации кости, с которой связан меш - model.GetBoneGlobalTransform(mesh.ParentBone.Index).
Это связано с тем, что в первом случае массив матриц итак содержит все необходимые трансформации костей.
Новый метод Draw
Наконец нам необходимо полностью переписать метод Draw, добавив туда выставление матриц проекции и вида для SkinnedEffect. А так же, собственно осуществить вызов нового метода DrawModel.
Поэтому перепишем Draw так:
Если мы запустим игру, то увидим следующее:
Нам удалось заменить капсулу на модель. Но она немного парит над землей. А всё дело в том, что DefaultY у нас стоит в 1. Меняем его на 0:
Теперь она стоит ровно на земле:
Текущая версия MyGame.cs:
Добавляем анимации
Объявим новое поле:
AnimationController - как и следует из названия - анимирует модели, вычисляя необходимые матрицы трансформации.
Теперь добавим код инициализации в LoadContent:
Здесь мы создаём AnimationController, привязываем его к _modelHero и запускаем анимацию Idle(бездействие). Мы её запускаем с флагом Looped, чтобы она перезапускалась по окончании.
Наконец добавим одну строку в метод Update:
Теперь модель будет проигрывать анимацию Idle на повторе:
Новый MyGame.cs:
Добавляем анимации бега и прыжка
Для начала добавим перечисление, обозначающие список возможных анимации:
Так же добавим очередную константу, её смысл я поясню в дальнейшем:
Добавляем поле текущей анимации:
Добавим в ProcessKeyboard код, меняющий анимации:
Собственно, за переключение анимаций отвечает вызов CrossfadeToClip.
Мы могли бы - как и в LoadContent - вызывать StartClip. Но тогда анимации бы резко сменялись одна на другую. CrossfadeToClip, же, делает это плавно в течении заданного времени. В нашем случае, это ранее заданная константа AnimationCrossfadeDelay, равная 200 мс.
Если мы запустим игру, то всё будет работать более ни менее правильно, кроме приземления. Поскольку анимация прыжка начнёт переключаться на анимацию бездействия только в момент приземления:
Мы бы хотели, чтобы анимация приземления начиналась в момент прыжка, когда персонаж падает и приближается к земле.
Для этого перепишем код UpdateJump так:
Мы вычисляем скорость прыжка(jumpVelocity), чтобы понять падаем ли мы(jumpVelocity < 0). Если падаем и упали ниже высоты 2, то включаем анимацию окончания прыжка.
Теперь анимация приземления выглядит как надо:
Да и бег выглядит нормально:
Новый MyGame.cs:
Цепляем меч на спину
Скачайте очередной архив и разархивируйте в папку проекта:
Он содержит модель меча.
Способ присоединения меча очень прост. Мы просто выберем одну из костей модели и применим её трансформацию на модели меча.
Перейдём к коду.
Добавим константу:
Это вручную подобранная локальная трансформация меча, прицепленного к спине.
Добавим поля:
Собственно, это модель меча и кость модели персонажа, к котором мы его прицепим.
Теперь добавим в LoadContent такой код(обязательно после инициализации _modelHero:
Здесь мы загружаем модель меча и в качестве кости присоединения выбираем спину персонажа.
Наконец добавим код в Draw:
Вначале мы применяем к модели ранее введённую локальную трансформацию. Дальше мы применяем к модели трансформацию кости спины. А затем всего персонажа.
В итоге меч оказывается на спине.
Заключение
Туториал окончен. Игра должна соответствовать видео в начале статьи.
Финальный MyGame.cs:
В следующей части мы изучим смешивание разных анимаций. И добавим доставание/убирание меча и взмах им.