This page has been translated automatically.
Unigine Basics
1. Introduction
2. Managing Virtual Worlds
3. Preparing 3D Models
4. Materials
5. Cameras and Lighting
6. Implementing Application Logic
7. Making Cutscenes and Recording Videos
8. Preparing Your Project for Release
9. Physics
10. Optimization Basics
12. PROJECT3: Third-Person Cross-Country Arcade Racing Game
13. PROJECT4: VR Application With Simple Interaction

Assembling a First-Person Setup With Controls

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:

  1. 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.

    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.

  2. 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.
  3. Go back to IDE and choose Project → Add Existing Item in the main menu.

  4. In the file dialog that opens select the files you've just copied (FirstPersonController.h and FirstPersonController.cpp) and click Add.
  5. 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.

    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.

    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.

  6. At the application startup the Component System shall generate the FirstPersonController property associated with the component. Close the application and get back to UnigineEditor.
  7. Create a new Object Dummy by choosing Create → Object → Dummy in the main menu, call it player and assign the FirstPersonController property to it.

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.

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.

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):

  1. 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.

  2. 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.

  3. Adjust the position of the player_hit_box so that it is placed immediately below the hands.
  4. 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:

We'll assign the Health component to it later.

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.

  1. Right-click the player Dummy Object and select Create → Camera → Dummy. Place the new created camera somwhere in the scene.

  2. 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.

  3. Add the hands Dummy Node as a child to the PlayerDummy node.

  4. 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.
  5. 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.
  6. Check the Main Player box in the parameters of the PlayerDummy to make it the main camera.

  7. 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.

    To avoid hands falling under gravity, adjust the position of the Capsule Shape to coincide with the position of the player_hit_box node.

  8. Select the player Dummy Object and go to the Node tab of the Parameters window.
  9. In the Node Components and Properties section, choose Camera → Camera mode → USE EXTERNAL.
  10. Drag and drop the PlayerDummy node to the Camera field.

    And make sure to check the Use Object Body in the Body group of the FirstPersonController component's parameters.

Now you can switch to the PlayerDummy camera in the Editor Viewport.

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.

  1. In the Asset Browser, find the data/fps/pistol/pistol.fbx asset and add it to the scene.
  2. 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).

  3. In the Bone drop-down list, select joint_hold. This will be the bone to which the pistol will be attached.

  4. Make the pistol a child of the WorldTransformBone node. Reset its relative position and rotation to zero if needed.

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:

  1. 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.
  2. Check the Loop option and click 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:

  1. Add a new HandAnimationController component class in the IDE (Project → Add Class).

    Copy and paste the following code to the corresponding files:

    HandAnimationController.h

    Source code (C++)
    #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);
    
    	// animation parameters
    	PROP_PARAM(File, idleAnimation);
    	PROP_PARAM(File, moveForwardAnimation);
    	PROP_PARAM(File, moveBackwardAnimation);
    	PROP_PARAM(File, moveRightAnimation);
    	PROP_PARAM(File, moveLeftAnimation);
    	PROP_PARAM(File, shootAnimation);
    
    	// declare methods to be called at the corresponding stages of the execution sequence
    	COMPONENT_INIT(init);
    	COMPONENT_UPDATE(update);
    
    	Unigine::Math::vec2 getLocalMovementVector();
    	void shoot();
    
    protected:
    	// world main loop overrides
    	void init();
    	void update();
    
    private:
    	FirstPersonController *fpsController = nullptr;
    
    	Unigine::ObjectMeshSkinnedPtr meshSkinned = nullptr;
    	float currentIdleWalkMix = 0.0f; // 0 idle animation, 1 walking animation
    	float currentShootMix = 0.0f; // 0 combination of idle/walking, 1 shooting animation
    	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;
    
    	// setting the number of animation layers
    	const int numLayers = 6;
    };

    HandAnimationController.cpp

    Source code (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);
    
    	// take the node to which the component is assigned
    	// and cast it to ObjectMeshSkinned type
    	meshSkinned = checked_ptr_cast<Unigine::ObjectMeshSkinned>(node);
    
    	// set the number of animation layers for each object
    	meshSkinned->setNumLayers(numLayers);
    
    	// set animation for each layer
    	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);
    
    	// enable all animation layers
    	for (int i = 0; i < numLayers; ++i)
    		meshSkinned->setLayerEnabled(i, true);
    }
    
    void HandAnimationController::shoot()
    {
    	// enable shooting animation
    	currentShootMix = 1.0f;
    	// set the animation layer frame to 0
    	currentShootFrame = 0.0f;
    }
    
    void HandAnimationController::update()
    {
    	vec2 movementVector = getLocalMovementVector();
    
    	// chack wether the character is moving
    	bool isMoving = movementVector.length2() > Math::Consts::EPS;
    	// input processing: checking if the 'fire' button is presse
    	bool isShooting = Input::isMouseButtonDown(Input::MOUSE_BUTTON_LEFT);
    	if (isShooting)
    		shoot();
    
    	// calculate target values for layer weights
    	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);
    
    	// apply current animation weights
    	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);
    
    	// update animation frames: set the same frame for all layers to ensure synchronization
    	meshSkinned->setLayerFrame(0, currentWalkIdleMixFrame);
    	meshSkinned->setLayerFrame(1, currentWalkIdleMixFrame);
    	meshSkinned->setLayerFrame(2, currentWalkIdleMixFrame);
    	meshSkinned->setLayerFrame(3, currentWalkIdleMixFrame);
    	meshSkinned->setLayerFrame(4, currentWalkIdleMixFrame);
    	// set the current frame for each animation layer to 0, to begin playback from the start
    	meshSkinned->setLayerFrame(5, currentShootFrame);
    
    	currentWalkIdleMixFrame += moveAnimationSpeed * Game::getIFps();
    	currentShootFrame = Math::min(currentShootFrame + shootAnimationSpeed * Game::getIFps(), (float)numShootAnimationFrames);
    
    	// smoothly update current weight values
    	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.

  2. In UnigineEditor, assign the property to the hands Skinned Mesh.
  3. Remove data/fps/hands/hands_animations/hands_pistol_idle.anim from the Preview Animation field of the Mesh Skinned section.
  4. Add animations stored in the data/fps/hands/hands_animations folder to the corresponding parameters.

  5. 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.

  6. Save all changes and run the application logic by hitting Run on the project's card in the SDK Browser to check the result.

Last update: 2024-11-06
Build: ()