Сборка персонажа с управлением
To make a main character, we will need a controller node implementing basic player functionality (handling keyboard and mouse input, movement settings, etc.). Attached to this node, we will have a first-person view camera, hands with a gun, and a body imitation to check for collisions with other characters, bullets, and environment. Later we will assign logic components to the nodes to implement shooting, visual effects, etc. Let's start with the character controller.Для создания главного персонажа, нам понадобится нода-контроллер, реализующая базовые функции игрока (обработка ввода с клавиатуры и мыши, настройки движения и т.д.). К этой ноде у нас будет привязана камера с видом от первого лица, руки с пистолетом и имитация тела для проверки столкновений с другими персонажами, пулями и окружающей средой. Позже мы назначим нодам логические компоненты для реализации стрельбы, визуальных эффектов и т.д., а пока займемся контроллером персонажа.
For the character controller, we will use the template first-person controller, that we're going to take from the Players sample included in the C++ Samples suite. Perform the following steps:Для этой цели мы будем использовать шаблон контроллера от первого лица, который мы возьмем из примера Players, входящего в набор C++ Samples. Для этого выполните следующие шаги:
-
Open the folder to which the C++ Samples suite was installed. Find the sample in the SDK Browser (Samples → Demos → C++ Samples) and click the Open Folder button.Откройте папку, в которую был установлен набор сэмплов, для этого найдите его в SDK Browser (Samples → Demos → C++ Samples) и щелкните по кнопке Open Folder.
In case there is no Open Folder on the card, most likely the samples suite is not yet installed. To install it click Install, wait until the installation process is completed and click Open Folder.Если кнопки Open Folder нет на карточке, значит набор примеров еще не установлен, для этого нажмите Install, дождитесь окончания установки и затем нажмите Open Folder.
- As the folder opens, go to the source\players subfolder and copy the FirstPersonController.h and the FirstPersonController.cpp files to your project's source folder.После того, как папка откроется, перейдите в подкаталог source\players и скопируйте из него файлы FirstPersonController.h и FirstPersonController.cpp в каталог source вашего проекта.
-
Go back to IDE and choose Project → Add Existing Item in the main menu.Вернитесь в IDE, выберите в меню Project → Add Existing Item.
- In the file dialog that opens select the files you've just copied (FirstPersonController.h and FirstPersonController.cpp) and click Add.В появившемся диалоге выберите только что скопированные файлы FirstPersonController.h и FirstPersonController.cpp и нажмите Add.
-
Now we can build the application. Don't forget to set the appropriate platform and configuration settings for your project before compiling your code in Visual Studio.Теперь можно собрать приложение. Не забудьте установить соответствующую платформу и параметры конфигурации для вашего проекта, прежде чем компилировать код в Visual Studio.
Build your application in Visual Studio (Build -> Build Solution) or otherwise, and launch it by selecting the project on the Projects tab of the SDK Browser and clicking Run.Соберите приложение в Visual Studio (Build -> Build Solution) или иным способом и запустите его, выбрав проект на вкладке Projects в браузере UNIGINE SDK и нажав Run.
Before running your application via the UNIGINE SDK Browser make sure, that appropriate Customize Run Options (Debug version in our case) are selected, by clicking an ellipsis under the Run button.Перед запуском приложения через браузер UNIGINE SDK убедитесь, что в Customize Run Options выбрана опция соответствующая типу сборки, который мы установили в IDE (в нашем случае Debug), для этого нажмите многоточие под кнопкой Run.
- At the application startup the Component System shall generate the FirstPersonController property associated with the component. Close the application and get back to UnigineEditor.При запуске приложения Компонентная система сгенерирует свойство (property) FirstPersonController связанное с компонентом. Закройте приложение и вернитесь в UnigineEditor.
- Create a new Object Dummy by choosing Create → Object → Dummy in the main menu, call it player and assign the FirstPersonController property to it.Создайте ноду Object Dummy, выбрав в меню Create → Object → Dummy, назовите ее player и назначьте ей свойство FirstPersonController.
A good start, so let's continue assembling our main character.Начало положено, давайте продолжать сборку персонажа.
Arranging a First-Person SetupНастройка вида от первого лица#
For a first-person setup you will need the hands and weapon models and animations previously created in a 3D modeling software. If you have your own assets, that's great, otherwise you can use our ready-to-use assets available in the data folder.Для настройки вида от первого лица нам понадобятся модели рук и оружия, а также анимации, ранее созданные в сторонней программе для 3D-моделирования. Если у вас заготовлены свои – хорошо, если нет, воспользуйтесь готовыми в папке data.
We'll start with adding hands, and then attaching a pistol to them. In Asset Browser, find the data/fps/hands/hands.fbx asset and add it to the scene.Итак, начнем с добавления рук, а затем прикрепим к ним пистолет. В Asset Browser найдите ассет data/fps/hands/hands.fbx и добавьте его в сцену.
To simulate a player's body that takes damage when hit by enemy bullets, let's create an additional object (it will approximate the player's body with a box):Чтобы имитировать тело игрока, в которое будут попадать вражеские пули, создадим дополнительный объект (в качестве упрощенной модели тела игрока будем использовать коробку):
-
In the Menu bar, choose Create → Primitive → Box to create a box primitive of the size (1,1,2), add it to the scene and rename to player_hit_box.В меню выберем Create → Primitive → Box, чтобы создать примитив box с размерами (1,1,2), добавим его в сцену и переименуем в player_hit_box.
-
Add it as a child to the hands Dummy Node and reset its position to the parent one. And for player_hit_box let's enable the Intersection option in the Surfaces section of the Parameters window.Затем добавим его в качестве дочернего элемента в Dummy Node hands и сбросим его положение на положение родительской ноды. А также включим для player_hit_box опцию Intersection в разделе Surfaces окна Parameters.
- Adjust the position of the player_hit_box so that it is placed immediately below the hands.Настроим положение player_hit_box так, чтобы он располагался непосредственно под руками.
-
Make it invisible by clearing its Viewport mask in the Node tab of the Parameters window using the Clear All button. Also clear the Shadow mask to disable shadows rendering. You'll get something like this:Затем сделаем его невидимым, очистив его маску Viewport на вкладке Node окна Parameters через Clear All, а также выключим маску Shadow, чтобы не рисовать для него тень. Должно получиться что-то в этом роде:
We'll assign the Health component to it later.Компонент Health назначим на него позже.
Adding a CameraДобавление камеры#
By default, FirstPersonController creates the camera during application execution, and to be able to see through the eyes of the character in UnigineEditor, you can create a new camera (PlayerDummy) and instruct the controller to use it. This will simplify testing of the first-person setup.По умолчанию FirstPersonController сам создает камеру в процессе выполнения приложения, а чтобы иметь возможность видеть глазами персонажа в UnigineEditor, можно создать новую камеру (PlayerDummy) и указать контроллеру использовать ее. Это облегчит тестирование настройки вида от первого лица.
-
Right-click the player Dummy Object and select Create → Camera → Dummy. Place the new created camera somwhere in the scene.Щелкните правой кнопкой мыши на Dummy Object player и выберите Create → Camera → Dummy. Поместите созданную камеру где-нибудь в сцене.
-
In the Node tab of the Parameters window, reset the position of the camera to the player position. Then adjust the rotation so that the camera is directed forward: set the rotation around the X axis to 90.Во вкладке Node в окне Parameters сбросьте положение камеры в позицию player. Затем отрегулируйте поворот так, чтобы камера была направлена вперед: установите вращение вокруг оси X равным 90.
-
Add the hands Dummy Node as a child to the PlayerDummy node.Добавьте Dummy Node hands в качестве дочерней ноды к ноде PlayerDummy.
- Adjust the position of the hands so that you can see them through the PlayerDummy camera. Transformation of the player's body should also be adjusted.Настройте положение рук так, чтобы вы могли видеть их через камеру PlayerDummy. Положение тела игрока также нужно будет откорректировать.
- In the Parameters window, change Near Clipping and FOV Degrees values of the PlayerDummy node: it will help you to get the required camera view.В окне Parameters измените значения Near Clipping и FOV Degrees ноды PlayerDummy: это поможет вам получить требуемый обзор камеры.
-
Check the Main Player box in the parameters of the PlayerDummy to make it the main camera.Включите опцию Main Player в параметрах PlayerDummy, чтобы сделать его основной камерой.
-
Select the player Dummy Object and go to the Physics tab of the Parameters window. Here add a Dummy body and a Capsule shape to the object.Выделите ноду Dummy Object player и перейдите на вкладку Physics окна Parameters. Здесь добавьте для объекта тело Dummy и коллизионную форму ShapeCapsule.
To avoid hands falling under gravity, adjust the position of the Capsule Shape to coincide with the position of the player_hit_box node.Чтобы руки не падали под действием силы тяжести, настройте положение ShapeCapsule так, чтобы оно совпадало с положением ноды player_hit_box.
- Select the player Dummy Object and go to the Node tab of the Parameters window.Выберите Dummy Object player и перейдите на вкладку Node окна Parameters.
- In the Node Components and Properties section, choose Camera → Camera mode → USE EXTERNAL.В разделе Node Components and Properties выберите Camera → Camera mode → USE EXTERNAL.
-
Drag and drop the PlayerDummy node to the Camera field.Перетащите ноду PlayerDummy в поле Camera.
And make sure to check the Use Object Body in the Body group of the FirstPersonController component's parameters.Также убедитесь, что у компонента FirstPersonController в группе параметров Body включена опция Use Object Body.
Now you can switch to the PlayerDummy camera in the Editor Viewport.Теперь можно переключиться на камеру PlayerDummy во вьюпорте редактора.
Attaching a Weapon to the HandsПрикрепление оружия к рукам#
In UNIGINE, you should use the Skinned Mesh with bones for animated models. Our FBX model of hands contains several bones. We can attach the object to a particular bone to make it follow this bone. For this purpose, we use a WorldTransformBone node that has a controlled object (a pistol in our case) as a child, and the Skinned Mesh with bones as its parent.В UNIGINE для анимированных моделей используется нода Skinned Mesh со скелетом. Наша FBX модель рук содержит набор костей. Можно прикрепить объект к определенной кости, чтобы заставить его следовать за этой костью. Для этого используется нода WorldTransformBone, дочерним объектом у которой должен быть управляемый объект (в нашем случае – пистолет), а родительским – Skinned Mesh со скелетом.
- In the Asset Browser, find the data/fps/pistol/pistol.fbx asset and add it to the scene.В Asset Browser найдите ассет data/fps/pistol/pistol.fbx и добавьте его в сцену.
-
In the Menu bar, choose Create -> Mesh -> SkinnedBone: a WorldTransformBone node will be created. Add it as a child to the hands Skinned Mesh (the one that is inherited from the hands Dummy Node).В меню выберите Create -> Mesh -> SkinnedBone: будет создана нода WorldTransformBone. Добавьте ее в качестве дочернего элемента в Skinned Mesh hands (являющийся дочерним элементом Dummy Node hands).
-
In the Bone drop-down list, select joint_hold. This will be the bone to which the pistol will be attached.В выпадающем списке Bone выберем joint_hold. Это будет кость, к которой мы прикрепим пистолет.
-
Make the pistol a child of the WorldTransformBone node. Reset its relative position and rotation to zero if needed.Сделаем пистолет дочерним элементом ноды WorldTransformBone. Сбросьте его относительное положение и поворот на ноль.
Testing AnimationsПроверка анимации#
There is also a number of animations to be used for the hands (idle, walking, shooting). You can check how a certain animation looks like, for example:У нас также есть набор готовых анимаций для рук (покой, ходьба, стрельба). Можно проверить, как выглядит каждая из них, например:
- In the Asset Browser, find the data/fps/hands/hands_animations/hands_pistol_idle.anim file and drag it to the Preview Animation section of the hands Skinned Mesh parameters.В Asset Browser найдите файл data/fps/hands/hands_animations/hands_pistol_idle.anim и перетащите его в раздел Preview Animation параметров Skinned Mesh hands.
-
Check the Loop option and click Play.Установите флажок Loop и нажмите Play.
Blending Animations and Playing Them via CodeСмешивание и воспроизведение анимаций с помощью кода#
When the character changes its states (shooting, walking forward/backward/left/right), the corresponding animations should change smoothly. Let's implement a component for mixing our animations.При изменении состояний персонажа (стрельба, ходьба вперед / назад / влево / вправо), соответствующие анимации должны плавно меняться. Напишем свой компонент для смешивания наших анимаций.
To ensure a seamless transition, we need to play two animations simultaneously and blend them. To do so, we will use multiple layers; we can assign different weights to each layer and achieve smooth blending.Чтобы получить плавный переход, нужно воспроизвести две анимации одновременно и смешать их. Для этого мы будем использовать несколько слоев; каждому из слоев можно назначить разные веса и добиться плавного смешивания.
The following scheme shows the blend tree we are going to use:На схеме ниже показан граф смешивания, который мы будем использовать:
-
Add a new HandAnimationController component class in the IDE (Project → Add Class).Добавьте в IDE новый компонент HandAnimationController (Project → Add Class).
Copy and paste the following code to the corresponding files:Скопируйте и вставьте следующий код в соответствующие файлы:
HandAnimationController.h
#pragma once #include <UnigineComponentSystem.h> #include <UnigineGame.h> #include "FirstPersonController.h" #include "ShootInput.h" class HandAnimationController : public Unigine::ComponentBase { public: COMPONENT_DEFINE(HandAnimationController, Unigine::ComponentBase); PROP_PARAM(Node, player_node, nullptr); PROP_PARAM(Float, moveAnimationSpeed, 30.0f); PROP_PARAM(Float, shootAnimationSpeed, 30.0f); PROP_PARAM(Float, idleWalkMixDamping, 5.0f); PROP_PARAM(Float, walkDamping, 5.0f); PROP_PARAM(Float, shootDamping, 1.0f); // параметры анимации PROP_PARAM(File, idleAnimation); PROP_PARAM(File, moveForwardAnimation); PROP_PARAM(File, moveBackwardAnimation); PROP_PARAM(File, moveRightAnimation); PROP_PARAM(File, moveLeftAnimation); PROP_PARAM(File, shootAnimation); // регистрация методов, вызываемых на соответствующих этапах World Logic COMPONENT_INIT(init); COMPONENT_UPDATE(update); Unigine::Math::vec2 getLocalMovementVector(); void shoot(); protected: // объявление методов, вызываемых на соответствующих этапах World Logic void init(); void update(); private: FirstPersonController *fpsController = nullptr; Unigine::ObjectMeshSkinnedPtr meshSkinned = nullptr; float currentIdleWalkMix = 0.0f; // 0 анимация покоя, 1 анимация ходьбы float currentShootMix = 0.0f; // 0 комбинация бездействие/ходьба, 1 анимация стрельбы float currentWalkForward = 0.0f; float currentWalkBackward = 0.0f; float currentWalkRight = 0.0f; float currentWalkLeft = 0.0f; float currentWalkIdleMixFrame = 0.0f; float currentShootFrame = 0.0f; int numShootAnimationFrames = 0; // задаем число анимационных слоев const int numLayers = 6; };
HandAnimationController.cpp
#include "HandAnimationController.h" REGISTER_COMPONENT(HandAnimationController); using namespace Unigine; using namespace Math; Unigine::Math::vec2 HandAnimationController::getLocalMovementVector() { return Math::vec2( Math::dot(fpsController->getSlopeAxisY(), fpsController->getHorizontalVelocity()), Math::dot(fpsController->getSlopeAxisX(), fpsController->getHorizontalVelocity()) ); } void HandAnimationController::init() { fpsController = ComponentSystem::get()->getComponent<FirstPersonController>(player_node); // берем ноду, которой назначена компонента // и преобразовываем ее к типу ObjectMeshSkinned meshSkinned = checked_ptr_cast<Unigine::ObjectMeshSkinned>(node); // устанавливаем количество анимационных слоев для каждого объекта meshSkinned->setNumLayers(numLayers); // устанавливаем анимацию для каждого слоя meshSkinned->setLayerAnimationFilePath(0, FileSystem::guidToPath(FileSystem::getGUID(idleAnimation.getRaw()))); meshSkinned->setLayerAnimationFilePath(1, FileSystem::guidToPath(FileSystem::getGUID(moveForwardAnimation.getRaw()))); meshSkinned->setLayerAnimationFilePath(2, FileSystem::guidToPath(FileSystem::getGUID(moveBackwardAnimation.getRaw()))); meshSkinned->setLayerAnimationFilePath(3, FileSystem::guidToPath(FileSystem::getGUID(moveRightAnimation.getRaw()))); meshSkinned->setLayerAnimationFilePath(4, FileSystem::guidToPath(FileSystem::getGUID(moveLeftAnimation.getRaw()))); meshSkinned->setLayerAnimationFilePath(5, FileSystem::guidToPath(FileSystem::getGUID(shootAnimation.getRaw()))); int animation = meshSkinned->getLayerAnimationResourceID(5); numShootAnimationFrames = meshSkinned->getLayerNumFrames(5); // включаем все анимационные слои for (int i = 0; i < numLayers; ++i) meshSkinned->setLayerEnabled(i, true); } void HandAnimationController::shoot() { // включаем анимацию стрельбы currentShootMix = 1.0f; // устанавливаем кадр анимационного слоя в 0 currentShootFrame = 0.0f; } void HandAnimationController::update() { vec2 movementVector = getLocalMovementVector(); // проверяем, движется ли персонаж bool isMoving = movementVector.length2() > Math::Consts::EPS; // обработка ввода: проверка нажатия клавиши 'огонь' bool isShooting = Input::isMouseButtonDown(Input::MOUSE_BUTTON_LEFT); if (isShooting) shoot(); // рассчитываем целевые значения для весовых коэффициентов слоев float targetIdleWalkMix = (isMoving) ? 1.0f : 0.0f; float targetWalkForward = (float)Math::max(0.0f, movementVector.x); float targetWalkBackward = (float)Math::max(0.0f, -movementVector.x); float targetWalkRight = (float)Math::max(0.0f, movementVector.y); float targetWalkLeft = (float)Math::max(0.0f, -movementVector.y); // применяем текущие весовые коэффициенты float idleWeight = 1.0f - currentIdleWalkMix; float walkMixWeight = currentIdleWalkMix; float shootWalkIdleMix = 1.0f - currentShootMix; meshSkinned->setLayerWeight(0, shootWalkIdleMix * idleWeight); meshSkinned->setLayerWeight(1, shootWalkIdleMix * walkMixWeight * currentWalkForward); meshSkinned->setLayerWeight(2, shootWalkIdleMix * walkMixWeight * currentWalkBackward); meshSkinned->setLayerWeight(3, shootWalkIdleMix * walkMixWeight * currentWalkRight); meshSkinned->setLayerWeight(4, shootWalkIdleMix * walkMixWeight * currentWalkLeft); meshSkinned->setLayerWeight(5, currentShootMix); // обновляем анимационные кадры: устанавливаем один и тот же кадр для всех слоев, чтобы обеспечить их синхронизацию meshSkinned->setLayerFrame(0, currentWalkIdleMixFrame); meshSkinned->setLayerFrame(1, currentWalkIdleMixFrame); meshSkinned->setLayerFrame(2, currentWalkIdleMixFrame); meshSkinned->setLayerFrame(3, currentWalkIdleMixFrame); meshSkinned->setLayerFrame(4, currentWalkIdleMixFrame); // устанавливаем текущий кадр для каждого анимационного слоя в 0, чтобы начать воспроизведение сначала meshSkinned->setLayerFrame(5, currentShootFrame); currentWalkIdleMixFrame += moveAnimationSpeed * Game::getIFps(); currentShootFrame = Math::min(currentShootFrame + shootAnimationSpeed * Game::getIFps(), (float)numShootAnimationFrames); // плавно обновляем текущие значения весовых коэффициентов currentIdleWalkMix = Math::lerp(currentIdleWalkMix, targetIdleWalkMix, idleWalkMixDamping * Game::getIFps()); currentWalkForward = Math::lerp(currentWalkForward, targetWalkForward, walkDamping * Game::getIFps()); currentWalkBackward = Math::lerp(currentWalkBackward, targetWalkBackward, walkDamping * Game::getIFps()); currentWalkRight = Math::lerp(currentWalkRight, targetWalkRight, walkDamping * Game::getIFps()); currentWalkLeft = Math::lerp(currentWalkLeft, targetWalkLeft, walkDamping * Game::getIFps()); currentShootMix = Math::lerp(currentShootMix, 0.0f, shootDamping * Game::getIFps()); }
Build and run the application by hitting Ctrl + F5 in your IDE to make the Component System generate a property to assign the component to nodes. Close the application after running and return to UnigineEditor.Соберите и запустите приложение, нажав в IDE Ctrl + F5, чтобы Компонентная система сгенерировала property для связи компонента с нодой. После запуска приложения закройте его и вернитесь в UnigineEditor.
- In UnigineEditor, assign the property to the hands Skinned Mesh.В UnigineEditor назначьте свойство (property) HandAnimationController на объект hands (Skinned Mesh).
- Remove data/fps/hands/hands_animations/hands_pistol_idle.anim from the Preview Animation field of the Mesh Skinned section.Удалите data/fps/hands/hands_animations/hands_pistol_idle.anim из поля Preview Animation в разделе Mesh Skinned.
-
Add animations stored in the data/fps/hands/hands_animations folder to the corresponding parameters.Добавьте анимации, хранящиеся в папке data/fps/hands/hands_animations, к соответствующим параметрам.
-
Assign (drag and drop) the player Dummy Object to the Player Node field of the HandAnimationController component so that it could get required data from the player's first person controller to perform blending.Назначьте (перетащите) Dummy Object player в поле Player Node компонента HandAnimationController, чтобы он мог получать необходимые данные из first person controller для смешивания анимаций.
-
Save all changes and run the application logic by hitting Run on the project's card in the SDK Browser to check the result.Сохраните все изменения и запустите логику приложения, нажав кнопку Run на карточке проекта в SDK Browser, чтобы проверить результат.