Published on

Have the Vroid model wield the sword [Unity / Universal Render Pipeline]

Authors
  • Evelyn

Please share this article if you like!

Vroid Studio is very useful, I'm making a game with 3D models made by Vroid.

I'm sure there are many of you out there.

In this article, I'll show you how to make your 3D Vroid model hold a sword and play the attack animation.

If you read this article to the end, you will be able to implement the following image.

I also explain in detail in the video, so if there are any parts that are difficult to understand in the blog post alone, please watch the Youtube video!

If you like it, I hope you will subscribe to my Youtube channel.

Now let's implement this feature!

Table of Contents

Use the Starter Asset to enable control of the Vroid model

First, let's create a new project on UnityHub!

I've created a Universal Render Pipeline project.

Next, we'll import the necessary packages.

We can use the assets called Starter Assets - Third Person Charater Controller to move the character to a third person perspective.

Starter Assets - Third Person Character Controlle

And then, we'll use a package called URP UniVRM to enable Unity to handle Vroid 3D models in the Universal Render Pipeline project.

URP UniVRM

And then, we'll use a package called URP UniVRM to enable Unity to handle Vroid 3D models in the Universal Render Pipeline project.

If you don't have a VRM file, you can use this 3d model.

Furthermore, open the Playground Scene in Starter Assets.

Since the shaders do not support the Universal Render Pipeline, select RenderPipeline from the Edit menu bar and change all shaders in the project to URP.

Edit > Render Pipeline > Universal Render Pipeline > Upgrade project Materials to UniversalRP Materials

Next, add a 3D model of the Vroid as a child element of the PlayerArmature, and hide the mesh and bones of the 3D model from the beginning.

You can also unpack the prefab and delete it.

Change the Avatar of the Animator component attached to the PlayerArmature to a Vroid avatar to enable control of the Vroid character.

With these simple steps, we can move a 3d character in a third-person perspective!

Adding an action

Since Starter Assets uses the latest Input System, we will add actions for attacks to the new Input System.

I find the new Input System very useful!

Open the InputSystem folder in Starter Assets, select Input Action Asset, and click the Edit Asset button in the inspector.

Next, follow the steps in the image to add an action for the attack.

In this case, I decided that the attack will be executed by right-clicking the mouse.

Binding operations can be modified as you like.

Since we have just banded the right mouse click, we will enable the KeyboardMouse checkbox in the control scheme.

Then, click on the Save Assets button and you are done adding the action.

Handling an attack eveent

Now it's time to tie the attack animation to the controller!

First, let's import the animation data for swinging the sword.

I also have this paid asset. It is very easy to use, so if you like the free version, you may want to purchase it.

Once the import is complete, add the Attack animation to the Animator Controller in Starter Assets - Third Person Character Controller by dragging and dropping.

In addition, we'll create a Transition and tie it to IdleWalkRunBlend.

Add the case where Jump is false to Conditions.

After that, we'll create another Transition and tie it to InAir.

Add the case where Jump is true to Conditions.

Now it is time to change the script.

Double-click ThirdPersonController to start VisualStudio.

And change the code as follows.

ThirdPersonController.cs

using System.Collections.Generic;
using UnityEngine;
#if ENABLE_INPUT_SYSTEM && STARTER_ASSETS_PACKAGES_CHECKED
using UnityEngine.InputSystem;
#endif

/* Note: animations are called via the controller for both the character and capsule using animator null checks
 */

namespace StarterAssets
{
  [RequireComponent(typeof(CharacterController))]
#if ENABLE_INPUT_SYSTEM && STARTER_ASSETS_PACKAGES_CHECKED
  [RequireComponent(typeof(PlayerInput))]
#endif
  public class ThirdPersonController : MonoBehaviour
  {
    [Header("Player")]
    [Tooltip("Move speed of the character in m/s")]
    public float MoveSpeed = 2.0f;
    [Tooltip("Sprint speed of the character in m/s")]
    public float SprintSpeed = 5.335f;
    [Tooltip("How fast the character turns to face movement direction")]
    [Range(0.0f, 0.3f)]
    public float RotationSmoothTime = 0.12f;
    [Tooltip("Acceleration and deceleration")]
    public float SpeedChangeRate = 10.0f;

    [Space(10)]
    [Tooltip("The height the player can jump")]
    public float JumpHeight = 1.2f;
    [Tooltip("The character uses its own gravity value. The engine default is -9.81f")]
    public float Gravity = -15.0f;

    [Space(10)]
    [Tooltip("Time required to pass before being able to jump again. Set to 0f to instantly jump again")]
    public float JumpTimeout = 0.50f;
    [Tooltip("Time required to pass before entering the fall state. Useful for walking down stairs")]
    public float FallTimeout = 0.15f;

    [Header("Player Grounded")]
    [Tooltip("If the character is grounded or not. Not part of the CharacterController built in grounded check")]
    public bool Grounded = true;
    [Tooltip("Useful for rough ground")]
    public float GroundedOffset = -0.14f;
    [Tooltip("The radius of the grounded check. Should match the radius of the CharacterController")]
    public float GroundedRadius = 0.28f;
    [Tooltip("What layers the character uses as ground")]
    public LayerMask GroundLayers;

    [Header("Cinemachine")]
    [Tooltip("The follow target set in the Cinemachine Virtual Camera that the camera will follow")]
    public GameObject CinemachineCameraTarget;
    [Tooltip("How far in degrees can you move the camera up")]
    public float TopClamp = 70.0f;
    [Tooltip("How far in degrees can you move the camera down")]
    public float BottomClamp = -30.0f;
    [Tooltip("Additional degress to override the camera. Useful for fine tuning camera position when locked")]
    public float CameraAngleOverride = 0.0f;
    [Tooltip("For locking the camera position on all axis")]
    public bool LockCameraPosition = false;

    // cinemachine
    private float _cinemachineTargetYaw;
    private float _cinemachineTargetPitch;

    // player
    private float _speed;
    private float _animationBlend;
    private float _targetRotation = 0.0f;
    private float _rotationVelocity;
    private float _verticalVelocity;
    private float _terminalVelocity = 53.0f;

    // timeout deltatime
    private float _jumpTimeoutDelta;
    private float _fallTimeoutDelta;

    // animation IDs
    private int _animIDSpeed;
    private int _animIDGrounded;
    private int _animIDJump;
    private int _animIDFreeFall;
    private int _animIDMotionSpeed;

    private Animator _animator;
    private CharacterController _controller;
    private StarterAssetsInputs _input;
    private GameObject _mainCamera;

    private const float _threshold = 0.01f;

    private bool _hasAnimator;

    private PlayerInput _playerInput;

    private List<string> _stopMoveAnimNames = new List<string>() { "Attack" };

    private void Awake()
    {
      // get a reference to our main camera
      if (_mainCamera == null)
      {
        _mainCamera = GameObject.FindGameObjectWithTag("MainCamera");
      }
    }

    private void Start()
    {
      _hasAnimator = TryGetComponent(out _animator);
      _controller = GetComponent<CharacterController>();
      _input = GetComponent<StarterAssetsInputs>();
      _playerInput = GetComponent<PlayerInput>();

      AssignAnimationIDs();

      // reset our timeouts on start
      _jumpTimeoutDelta = JumpTimeout;
      _fallTimeoutDelta = FallTimeout;
    }

    private void Update()
    {
      _hasAnimator = TryGetComponent(out _animator);

      JumpAndGravity();
      GroundedCheck();
      Move();

      if (_playerInput.actions["Attack"].ReadValue<float>() > 0f)
      {
        _animator.Play("Attack");
      }
    }

    private void LateUpdate()
    {
      CameraRotation();
    }

    private void AssignAnimationIDs()
    {
      _animIDSpeed = Animator.StringToHash("Speed");
      _animIDGrounded = Animator.StringToHash("Grounded");
      _animIDJump = Animator.StringToHash("Jump");
      _animIDFreeFall = Animator.StringToHash("FreeFall");
      _animIDMotionSpeed = Animator.StringToHash("MotionSpeed");
    }

    private void GroundedCheck()
    {
      // set sphere position, with offset
      Vector3 spherePosition = new Vector3(transform.position.x, transform.position.y - GroundedOffset, transform.position.z);
      Grounded = Physics.CheckSphere(spherePosition, GroundedRadius, GroundLayers, QueryTriggerInteraction.Ignore);

      // update animator if using character
      if (_hasAnimator)
      {
        _animator.SetBool(_animIDGrounded, Grounded);
      }
    }

    private void CameraRotation()
    {
      // if there is an input and camera position is not fixed
      if (_input.look.sqrMagnitude >= _threshold && !LockCameraPosition)
      {
        _cinemachineTargetYaw += _input.look.x * Time.deltaTime;
        _cinemachineTargetPitch += _input.look.y * Time.deltaTime;
      }

      // clamp our rotations so our values are limited 360 degrees
      _cinemachineTargetYaw = ClampAngle(_cinemachineTargetYaw, float.MinValue, float.MaxValue);
      _cinemachineTargetPitch = ClampAngle(_cinemachineTargetPitch, BottomClamp, TopClamp);

      // Cinemachine will follow this target
      CinemachineCameraTarget.transform.rotation = Quaternion.Euler(_cinemachineTargetPitch + CameraAngleOverride, _cinemachineTargetYaw, 0.0f);
    }

    private void Move()
    {
      // set target speed based on move speed, sprint speed and if sprint is pressed
      float targetSpeed = _input.sprint ? SprintSpeed : MoveSpeed;

      // a simplistic acceleration and deceleration designed to be easy to remove, replace, or iterate upon

      // note: Vector2's == operator uses approximation so is not floating point error prone, and is cheaper than magnitude
      // if there is no input, set the target speed to 0
      if (_input.move == Vector2.zero) targetSpeed = 0.0f;

      // a reference to the players current horizontal velocity
      float currentHorizontalSpeed = new Vector3(_controller.velocity.x, 0.0f, _controller.velocity.z).magnitude;

      float speedOffset = 0.1f;
      float inputMagnitude = _input.analogMovement ? _input.move.magnitude : 1f;

      // accelerate or decelerate to target speed
      if (currentHorizontalSpeed < targetSpeed - speedOffset || currentHorizontalSpeed > targetSpeed + speedOffset)
      {
        // creates curved result rather than a linear one giving a more organic speed change
        // note T in Lerp is clamped, so we don't need to clamp our speed
        _speed = Mathf.Lerp(currentHorizontalSpeed, targetSpeed * inputMagnitude, Time.deltaTime * SpeedChangeRate);

        // round speed to 3 decimal places
        _speed = Mathf.Round(_speed * 1000f) / 1000f;
      }
      else
      {
        _speed = targetSpeed;
      }
      _animationBlend = Mathf.Lerp(_animationBlend, targetSpeed, Time.deltaTime * SpeedChangeRate);

      // normalise input direction
      Vector3 inputDirection = new Vector3(_input.move.x, 0.0f, _input.move.y).normalized;

      // note: Vector2's != operator uses approximation so is not floating point error prone, and is cheaper than magnitude
      // if there is a move input rotate player when the player is moving
      if (_input.move != Vector2.zero)
      {
        _targetRotation = Mathf.Atan2(inputDirection.x, inputDirection.z) * Mathf.Rad2Deg + _mainCamera.transform.eulerAngles.y;
        float rotation = Mathf.SmoothDampAngle(transform.eulerAngles.y, _targetRotation, ref _rotationVelocity, RotationSmoothTime);

        // rotate to face input direction relative to camera position
        transform.rotation = Quaternion.Euler(0.0f, rotation, 0.0f);
      }


      Vector3 targetDirection = Quaternion.Euler(0.0f, _targetRotation, 0.0f) * Vector3.forward;

      // move the player
      if (_animator.GetCurrentAnimatorClipInfo(0).Length > 0 && _stopMoveAnimNames.Contains(_animator.GetCurrentAnimatorClipInfo(0)[0].clip.name))
      {
        targetDirection.x = 0;
        targetDirection.z = 0;
      }
      _controller.Move(targetDirection.normalized * (_speed * Time.deltaTime) + new Vector3(0.0f, _verticalVelocity, 0.0f) * Time.deltaTime);

      // update animator if using character
      if (_hasAnimator)
      {
        _animator.SetFloat(_animIDSpeed, _animationBlend);
        _animator.SetFloat(_animIDMotionSpeed, inputMagnitude);
      }
    }

    private void JumpAndGravity()
    {
      if (Grounded)
      {
        // reset the fall timeout timer
        _fallTimeoutDelta = FallTimeout;

        // update animator if using character
        if (_hasAnimator)
        {
          _animator.SetBool(_animIDJump, false);
          _animator.SetBool(_animIDFreeFall, false);
        }

        // stop our velocity dropping infinitely when grounded
        if (_verticalVelocity < 0.0f)
        {
          _verticalVelocity = -2f;
        }

        // Jump
        if (_input.jump && _jumpTimeoutDelta <= 0.0f)
        {
          // the square root of H * -2 * G = how much velocity needed to reach desired height
          _verticalVelocity = Mathf.Sqrt(JumpHeight * -2f * Gravity);

          // update animator if using character
          if (_hasAnimator)
          {
            _animator.SetBool(_animIDJump, true);
          }
        }

        // jump timeout
        if (_jumpTimeoutDelta >= 0.0f)
        {
          _jumpTimeoutDelta -= Time.deltaTime;
        }
      }
      else
      {
        // reset the jump timeout timer
        _jumpTimeoutDelta = JumpTimeout;

        // fall timeout
        if (_fallTimeoutDelta >= 0.0f)
        {
          _fallTimeoutDelta -= Time.deltaTime;
        }
        else
        {
          // update animator if using character
          if (_hasAnimator)
          {
            _animator.SetBool(_animIDFreeFall, true);
          }
        }

        // if we are not grounded, do not jump
        _input.jump = false;
      }

      // apply gravity over time if under terminal (multiply by delta time twice to linearly speed up over time)
      if (_verticalVelocity < _terminalVelocity)
      {
        _verticalVelocity += Gravity * Time.deltaTime;
      }
    }

    private static float ClampAngle(float lfAngle, float lfMin, float lfMax)
    {
      if (lfAngle < -360f) lfAngle += 360f;
      if (lfAngle > 360f) lfAngle -= 360f;
      return Mathf.Clamp(lfAngle, lfMin, lfMax);
    }

    private void OnDrawGizmosSelected()
    {
      Color transparentGreen = new Color(0.0f, 1.0f, 0.0f, 0.35f);
      Color transparentRed = new Color(1.0f, 0.0f, 0.0f, 0.35f);

      if (Grounded) Gizmos.color = transparentGreen;
      else Gizmos.color = transparentRed;

      // when selected, draw a gizmo in the position of, and matching radius of, the grounded collider
      Gizmos.DrawSphere(new Vector3(transform.position.x, transform.position.y - GroundedOffset, transform.position.z), GroundedRadius);
  }
}
If you only want to know the difference, please watch the Youtube video!

Now the attack animation will play when you right-click the mouse.

Making a 3D model hold a sword

Finally, let's put a sword in your character's right hand!

Add the 3D model of the sword included in Melee Warrior Animations FREE to the bone of your character's right hand!

Then, adjust the Rotation and Position of the Transform.

In the same way that we modified the shaders in the StarterAssets, we will optimize the shaders of the sword material for the Universal Render Pipeline.

Edit > Render Pipeline > Universal Render Pipeline > Upgrade project Materials to UniversalRP Materials

If you've done all this work and run it, you should see something like the following.

Wrap up

This was a bit of a long article, but what do you think?

The Youtube video is only about 6 minutes long, so if there are any operations you don't understand in the blog post, please watch the video.

In this article, we have dealt with the following topics.

Details of this work
  • Using Starter Assets - Third Person Character Controller to run a Vroid 3D model
  • Add an attack action to the new Input System and right-click the mouse.
  • Incorporate attack animations and InputSystem actions into ThirdPersonController
  • Put a sword in the character's right hand.

I hope the content of this article has been helpful to you.

Thank you for reading to the end.

Please share this article if you like!