Пишем Third Person Controller на MonoGame. Часть I
Введение
В этой серии туториалов мы реализуем простой Third Person Controller на базе MonoGame.
Серия рассчитана на читателей, уже знакомых с основами MonoGame и 3D-графики.
Для комфортного понимания материала желательно разбираться в следующих темах:
- C#
- MonoGame
- Основы 3D-геометрии — матрицы, вектора, преобразования
- Понимание работы камеры в 3D-сцене и того, что такое `world`, `view` и `projection` матрицы
- Базовый 3D-рендеринг в MonoGame (`BasicEffect`, `SkinnedEffect`)
- Скелетная анимация (для первой части необязательно)
Если с чем-то из этого списка вы пока не знакомы — в Интернете достаточно хороших материалов. В частности, рекомендую знаменитые Reimers Tutorials:
DigitalRiseModel
На протяжении всей серии мы будем использовать мою библиотеку:
Это попытка сделать более удобное API для работы с 3D-моделями в MonoGame.
Библиотека позволяет:
- Создавать 3D-примитивы (кубы, сферы, капсулы и т.д.) прямо в коде
- Загружать модели в форматах `gltf/glb`
- Работать со скелетной анимацией
No Content Pipeline
Также важно отметить, что в этой серии мы не будем использовать Content Pipeline.
Почему я предпочитаю не использовать Content Pipeline, я подробно описал здесь:
Вместо него мы будем использовать другую мою библиотеку:
Она позволяет загружать ассеты напрямую в «сыром» виде.
Часть I
В первой части герой будет представлен в виде капсулы. Мы реализуем рендеринг, движение и прыжки.
Итоговый результат будет выглядеть так:
В следующих частях мы заменим капсулу на полноценную модель персонажа и добавим анимации.
Создание проекта
Создайте новый MonoGame-проект под любую платформу (например DesktopGL).
После этого добавьте NuGet-пакет:
Он автоматически подтянет и XNAssets.
Скачайте этот архив Assets.zip
Затем распакуйте его (он состоит всего из одного файла `checker.dds`) в папку проекта и добавьте в `.csproj` следующий код:
Таким образом ассеты нашего проекта будут всегда копироваться в Output Directory.
Инициализация и загрузка контента
Для простоты весь код мы будем писать прямо внутри нашего `Game`-класса.
Для начала добавим константу, задающую стандартную высоту персонажа:
Теперь объявим следующие поля:
DrMesh — это класс из DigitalRiseModel. По сути он является аналогом обычного ModelMesh из MonoGame.
Теперь реализуем LoadContent:
Здесь всё довольно прямолинейно:
- загружаем текстуру земли
- создаём меш земли
- создаём капсулу персонажа
- настраиваем освещение
- задаём стартовую позицию героя
Вспомогательные методы
Добавим пару утилитных методов: DrawMesh и ToMatrix
DrawMesh
Метод рисует меш с заданной матрицей трансформации, цветом и текстурой.
ToMatrix
А этот метод собирает стандартную матрицу трансформации.
Важно понимать порядок операций: Scale -> Rotate -> Translate
В MonoGame/XNA матрицы перемножаются слева направо, поэтому объект сначала масштабируется, затем вращается и только потом перемещается в мир.
Hero, Camera Mount и Camera
Сразу предупреждаю, что это самый сложный раздел в туториале.
Мы хотим получить классическую third-person камеру, которая:
- Всегда следует за персонажем
- Позволяет вращать персонажа мышкой по горизонтали
- Позволяет наклонять камеру вверх-вниз
Другими словами, мы хотим такую конструкцию:
Она состоит из 3 объектов:
- Hero (зелёный) - персонаж
- Camera Mount(оранжевый) — жёсткий "штатив", прикреплённый к голове персонажа. У его самого основания есть шарнир, позволяющий вращение вверх-вниз
- Camera - сама камера, которая прикреплена к другому концу штатива. Она всегда повернута на 180 градусов по оси Y, дабы смотреть на спину персонажа
Вся эта конструкция задаётся тремя переменными:
- Положение Hero в мире (`Vector3 _heroPosition`)
- Угол поворота Hero вокруг оси Y (`float _heroYaw`)
- Угол поворота Camera Mount вокруг оси X (`float _cameraMountPitch`)
Первую мы уже задали. Теперь добавим остальные:
Вся предложенная конструкция является иерархией трансформаций.
Т.е. если мы хотим вычислить итоговую трансформацию камеры(а мы этого хотим, дабы вычислить матрицу camera view), необходимо вычислить трансформации всех объектов цепочки.
Вычисление иерархии трансформации и рендеринг сцены
Объявим константы камеры:
Теперь можно реализовать Draw, в котором и будет вычисляться вся иерархия трансформаций:
Трансформация камеры вычисляется поэтапно.
Сначала вычисляется трансформация персонажа heroTransform.
Затем на его основании вычисляется cameraMountTransform. Он смешён на 1 по оси Y(cameraMountOffset), дабы быть на уровне головы.
После этого вычисляется cameraTransform. Он смещен на -5 по оси Y и повёрнут на 180 градусов, чтобы на некотором расстоянии всегда смотреть на спину персонажа.
Почему View Matrix инвертируется
Рассмотрим эту строку:
Важно понимать:
View Matrix — это не transform камеры.
Наоборот, это матрица, которая преобразует мир относительно камеры.
Поэтому для получения View Matrix необходимо инвертировать мировую трансформацию камеры.
Промежуточный итог
Если мы запустим игру сейчас, то получим следующую картинку:
Рендеринг уже работает, однако камера пока остаётся неподвижной.
Движение камеры
Добавим константу, обозначающую чувствительность мышки:
Добавим поле для хранения последнего состояния мышки:
Теперь в Update добавим следующий код:
Сам по себе код весьма очевиден. Мы меняем ранее заданные _heroYaw и _cameraMountPitch на соотвествующие изменения координаты мышки(горизонтальную для _heroYaw и вертикальную для _cameraMountPitch).
Запустим игру и убедимся, что вращение камеры работает:
Движение персонажа
Добавим константу, обозначающую скорость движения:
Теперь в Update добавим код, который осуществляет это самое движение при нажатии клавиш WASD:
Здесь мы вычисляем матрицу трансформации Hero, дабы получить из неё вектора Forward(нужен для движения вперёд-напад) и Right(для движения влево-вправо). Далее мы используем эти вектора, чтобы рассчитать новое положение персонажа.
Запустим игру и убедимся, что движение персонажа работает:
Прыжки
Последнее, что мы добавим - это прыжки.
Сразу оговоримся, что мы хотим, чтобы при прыжках сохранялась инерция движения.
Начнём как обычно с задания новых констант:
А так же пары полей, задающих состояние прыжка
Наконец перепишем вышеприведённый код движения так:
Всё достаточно очевидно. При нажатии пробела, мы устанавливаем время начала прыжка и инерцию.
В самом же прыжке, мы рассчитываем высоту персонажа по известной со школьных времён формуле движения с ускорением. Когда мы падаем ниже DefaultY, то оканчиваем прыжок.
Туториал окончен. Наша игра должна соответствовать видео из начала этой статьи.
Заключение
Полный исходный код этой части можно посмотреть здесь: