组装第一人称设置与控制
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:对于角色控制器,我们将使用模板化的第一人称控制器,该模板取自C++ Samples套件中包含的Players示例。请执行以下步骤:
-
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.打开已安装C++示例套件的文件夹。在SDK浏览器中找到该示例(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),或者通过其他方式构建,然后在SDK浏览器的Projects选项卡中选择项目并点击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浏览器运行应用程序前,请确保点击Run按钮下的省略号选择了适当的Customize Run Options(本例中为Debug版本)。
- At the application startup the Component System shall generate the FirstPersonController property associated with the component. Close the application and get back to UnigineEditor.应用程序启动时,组件系统将生成与组件关联的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.通过主菜单选择Create → Object → Dummy创建一个新的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.首先添加手部模型,随后为其装配手枪。在资源浏览器中找到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(创建→基本体→长方体),创建尺寸为(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.将其作为hands虚拟节点(Dummy Node)的子物体,并将位置重置为父级坐标。在Parameters(参数)窗口的Surfaces(曲面)属性中,为player_hit_box启用Intersection(碰撞检测)选项。
- 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:在Parameters(参数)窗口的Node(节点)标签页中,使用Clear All(清除全部)按钮清空其Viewport(视口)蒙版以实现隐形。同时清除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.右键点击playerDummy Object(虚拟对象),选择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.在Parameters(参数)窗口的Node(节点)选项卡中,将摄像机位置重置为player位置。然后调整旋转使摄像机朝向前方:将X轴旋转设置为90度。
-
Add the hands Dummy Node as a child to the PlayerDummy 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.在参数窗口中修改PlayerDummy节点的近裁剪面(Near Clipping)和视野角度(FOV Degrees)值,这将帮助你获得理想的摄像机视图。
-
Check the Main Player box in the parameters of the PlayerDummy to make it the main camera.勾选PlayerDummy参数中的Main Player选项,将其设为主摄像机。
-
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.选中playerDummy Object(虚拟对象),进入Parameters(参数)窗口的Physics(物理)选项卡。在此为该对象添加虚拟物理体和胶囊碰撞体。
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.选中playerDummy Object,进入Parameters(参数)窗口的Node(节点)选项卡。
- 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节点:以受控对象(本例中的手枪)作为子节点,以带有骨骼的蒙皮网格作为父节点。
- In the Asset Browser, find the data/fps/pistol/pistol.fbx asset and add it to the scene.在资源浏览器中找到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节点。将该节点作为hands蒙皮网格(继承自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.在资源浏览器中找到data/fps/hands/hands_animations/hands_pistol_idle.anim文件。将其拖拽到hands蒙皮网格参数的Preview Animation(预览动画)区域。
-
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
源代码 (C++)#pragma once #include <UnigineComponentSystem.h> #include <UnigineGame.h> #include "FirstPersonController.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
源代码 (C++)#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中,将HandAnimationController属性(property)分配给hands蒙皮网格。
- Remove data/fps/hands/hands_animations/hands_pistol_idle.anim from the Preview Animation field of the Mesh Skinned section.从蒙皮网格部分的Preview Animation(预览动画)字段中移除data/fps/hands/hands_animations/hands_pistol_idle.anim。
-
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.将player Dummy Object拖拽到HandAnimationController组件的Player Node字段,使其能从player的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.保存所有更改,然后在SDK Browser中点击项目卡片上的Run按钮运行应用程序逻辑以检查结果。
本页面上的信息适用于 UNIGINE 2.20 SDK.