Published on

[Unity] Pick up an items on the ground (Vroid / StarterAssets)

Authors
  • Evelyn

Please share this article if you like!

Hey, guys! I'm Evelyn.

When you are making a game, you may want to have your character hold an item in his or her hand.

In this blog post, we'll be judging between the character controller and items on the ground, and implementing how to hold the items in your hands.

This is assumed that the implementation of the following article has been completed.

The same content is explained in the Youtube video, so please refer to the video if there are parts that are not clear in the blog post.


Table of Contents

Implementing pickup

First, create a 3D text above the 3D character's head.

Create an empty game object at the location of the character's right hand bone and name it ItemSlot.

Then, copy the Transform value of the sword and paste the value into the Transform of the ItemSlot.

Place the 3D object of the sword on the ground and change the value of Transform. In addition, add a Box collider.

Then create a new tag called Item and change the tag to Item.

Next, we'll make some changes to the code. Since we are using the Starter Assets Third Person Character Controller, we'll edit ThirdPersonController.cs.

Add a variable.

[SerializeField] GameObject PickupAnnounce;
[SerializeField] Transform ItemSlot;
private bool _canPickupFlag = false;
private GameObject _pickupTarget;
private GameObject _grabbingItem;

OnControllerColliderHit method is used to determine the collision of the Character Controller.

private void OnControllerColliderHit(ControllerColliderHit hit)
{
    bool itemFlag = hit.gameObject.CompareTag("Item");
    PickupAnnounce.SetActive(itemFlag);
    _canPickupFlag = itemFlag;
    if (itemFlag)
    {
      _pickupTarget = hit.gameObject;
    }
}

In addition, a decision is made in the Update method.

if (Keyboard.current != null && Keyboard.current.rKey.wasReleasedThisFrame)
{
  if (_grabbingItem)
  {
    _grabbingItem.transform.SetParent(null);
    _grabbingItem.GetComponent<BoxCollider>().isTrigger = false;
    _grabbingItem.transform.localEulerAngles = Vector3.zero;
    _grabbingItem.transform.position = transform.position;
    _grabbingItem = null;
  }
  if (_canPickupFlag && _pickupTarget)
  {
    // TODO: you can play pickup animation here
    _grabbingItem = _pickupTarget;
    _pickupTarget = null;
    _grabbingItem.GetComponent<BoxCollider>().isTrigger = true;
    _grabbingItem.transform.SetParent(ItemSlot);
    _grabbingItem.transform.localEulerAngles = Vector3.zero;
    _grabbingItem.transform.localPosition = Vector3.zero;
  }
}

The following is the entire code of ThirdPersonController.cs.

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" };

    [SerializeField] GameObject PickupAnnounce;
    [SerializeField] Transform ItemSlot;
    private bool _canPickupFlag = false;
    private GameObject _pickupTarget;
    private GameObject _grabbingItem;

    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");
      }

      if (Keyboard.current != null && Keyboard.current.rKey.wasReleasedThisFrame)
      {
        if (_grabbingItem)
        {
          _grabbingItem.transform.SetParent(null);
          _grabbingItem.GetComponent<BoxCollider>().isTrigger = false;
          _grabbingItem.transform.localEulerAngles = Vector3.zero;
          _grabbingItem.transform.position = transform.position;
          _grabbingItem = null;
        }
        if (_canPickupFlag && _pickupTarget)
        {
          // TODO: you can play pickup animation here
          _grabbingItem = _pickupTarget;
          _pickupTarget = null;
          _grabbingItem.GetComponent<BoxCollider>().isTrigger = true;
          _grabbingItem.transform.SetParent(ItemSlot);
          _grabbingItem.transform.localEulerAngles = Vector3.zero;
          _grabbingItem.transform.localPosition = Vector3.zero;
        }
      }
    }

    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);
    }

    private void OnControllerColliderHit(ControllerColliderHit hit)
    {
        bool itemFlag = hit.gameObject.CompareTag("Item");
        PickupAnnounce.SetActive(itemFlag);
        _canPickupFlag = itemFlag;
        if (itemFlag)
        {
          _pickupTarget = hit.gameObject;
        }
    }
  }
}

Finally, assign values to the variables you added to ThirdPersonController.cs to complete the process.

Once implemented so far, you should be able to move items into the character's hands.

Wrap up

Character collision detection can be applied to many things, so please try to incorporate it into your projects.

I hope I have been able to be of some help to you in your game development.

Please share this article if you like!