Tag: physics

Pick Up and Throw – Materials, Scale and Selection Updates

Pick Up and Throw – Materials, Scale and Selection Updates

After this and this recent post, the player PickUpandThrow mechanic has now been updated to address some issues including:

  • Collisions when ‘holding’ an interactable object
  • Better throwing/firing mechanics
  • Selection outline for interactables
  • Swapping materials

There are a few other updates, especially related to destructable objects that I’ll cover elsewhere.

Pick up and Throw updates

Materials:

One of the main problems I wanted to address in this update was how to change the PBR material of the interactable object so that when it is held, the player can ‘see through’ it. This was no porblem with just using the base color option of the attached material but when using an Albedo texture, an alpha animation was more problematic.

My first effort was to create a custom shader using the Unity ShaderGraph – a very neat tool that I’ll no doubt come back to at a later point. While creating a basic PBR setup with Albedo, Smooth, Metallic, AO etc channels was straight forward there seems to be an issue with the way in which SG deals with normal map calculation (see this unity forum post). So while I was able to create an alpha slider for the albedo channel and expose that parameter in the Inspector, the normal mapping was a bit weird.

That led me into Blender at first and experimenting with trying to UV unwrap and export a project specific cube/sphere (which in turn led to this post) but I was still stuck with the normals issue.

The solution (at the mo) is Unity’s built in URP lit shader that includes Albedo, Metallic, Normal and AO channels that render nicely but accessing an alpha solution was a bit of a pain. In the end I opted for a second (lit shader) material slot in the Object class that was a copy of the original apart from the surface type set to Transparent instead of Opaque. Transparent materials have easy access to the alpha channel (in the same way as using just the color channel) that in turn can be run through an Animator component in order to achieve the fade up/down effect I need.

In the Object class, the relevant lines look like this:

rigidBodyObjects_Interactions()

Start()
{
    //Set the 2 materials in the Inspector:
    [Header("Materials")]
    public Material defaultMaterial;
    public Material alphaMaterial;
}

Update()
{
            //if is holding this object:
            if (pickUpAndThrow.isHoldingObject)
            {
                if (pickUpAndThrow.hitObjectIsAtHand)
                {
                    //switch off outline:
                    outline.enabled = false;

                    //change material surface type to transparent:
                    renderer.material = alphaMaterial;

                    //fade down alpha animation:
                    animator.SetBool("isHolding", true);

                }

                if (!pickUpAndThrow.hitObjectIsAtHand)
                {
                    //switch on outline:
                    outline.enabled = true;

                    //change surface to opaque
                    renderer.material = defaultMaterial;

                    //fade up alpha animation:
                    animator.SetBool("isHolding", false);
                }
}

I’m not posting the whole code here as I’m only interested in what has changed compared to the script posted in Change Color On Raycast Hit.

The main changes are using the second (transparent) material and accessing the animator component that simply fades the alpha value of the material.

There may be issues in the future as transparent materials are expensive, plus I’m not altogether sure if I’m spending resources in duplicating shaders here, but as this effect is applied to one object at a time, and based on my current tests it does the job for now.

Selection Highlighting

I wanted to add an outline to selectable objects to indicate that they can be picked up. Fortunately this solution was an easy (if lazy) one. After wrestling with the SG once again, and coming to the conclusion that while fresnel was a great option for spheres, it wasn’t so effective with cubes – what with it being fresnel!!!! – I found this FREE asset that was easy to integrate, access and control from the Object class.

While I do try my best not to rely on asset store resources this plug in is great…and I’m using it!!!!

One Thing At A Time!

Another easy fix (that should also save a bit of performance…until I need multi-tasking interactions!!!) was to limit the raycast call from the player so there are no other raycast trigger events while holding an object.

This fix is in the PlayerManager() class, in Update():

        //Only perform a raycast if NOT
        //holding an interactable object
        if (!pickUpandThrow.hitObjectIsAtHand)
        { rayCastHit = rayCast.DoRaycast(cam, scanRange); }

Collisons and Physics

In this final section of this post I want to highlight the physics interactions that include collisions, throwing, pick up move/rotate and moving playerhand based on object scale.

As an additional note, this unity answers post was a help in determining the rotation/position maths. The main methods to get a grip of are the MovePosiiton() and RotatePosition() calculations that eliminate the isKinematic calls I was making before.

Fortunately for me I’ve been employing good practice and the comments in the PickUpandThrow class, (that manages all related interactions/mechanics) explain the whole process in a much more concise and demonstrable manner than I would manage using this text…

private void FixedUpdate()
    {
        if (input.isInteracting && !isHoldingObject)
        {
            //we can pick up the object if we can see it:
            if (playerManager.rayCastHit != null
                && playerManager.rayCastHit.CompareTag("Interactable"))
            {
                //set the RB of target object received from the raycast
                //in PlayerManager:
                hitObjectRigidBody =
                   playerManager.rayCastHit.GetComponent<Rigidbody>();

                //move the player hand forward by size of
                //object * offset
                hitObjectScale = hitObjectRigidBody.transform.localScale;

                playerHand.transform.localPosition = new Vector3(
                    0, 0, hitObjectScale.z * playerHandOffset);

                //toggle interact button
                input.isInteracting = false;

                //we are now holding the object
                isHoldingObject = true;
            }
        }

        //Pick up the object
        if (isHoldingObject && !hitObjectIsAtHand)
        {
            //move towards player hand
            hitObjectRigidBody.MovePosition(Vector3.MoveTowards(
                hitObjectRigidBody.position,
                playerHand.position,
                pickUpSpeed * Time.fixedDeltaTime));

            //rotate towards zero:
            Vector3 targetDirection =
                (hitObjectRigidBody.position - Vector3.zero).normalized;

            Quaternion targetRotation = Quaternion.LookRotation(targetDirection);

            hitObjectRigidBody.MoveRotation(Quaternion.RotateTowards
                (hitObjectRigidBody.transform.rotation, targetRotation,
                pickUpSpeed * Time.fixedDeltaTime));

            //turn off gravity
            hitObjectRigidBody.useGravity = false;
        }

        if (isHoldingObject && hitObjectIsAtHand)
        {
            //if object has reached player hand disable its movement
            //by 'teleporting it into position. 
            hitObjectRigidBody.MovePosition(playerHand.position);

            //is the object is != rotation zero
            //slow rotation by lerping the angular velocity
            //to zero:
            if (hitObjectRigidBody.rotation
                != Quaternion.Euler(Vector3.zero))
            {
                hitObjectRigidBody.angularVelocity = Vector3.Lerp
                    (hitObjectRigidBody.angularVelocity,
                    Vector3.zero,
                    pickUpSpeed * Time.fixedDeltaTime);
            }
        }

        if (input.isInteracting && isHoldingObject)
        {
            //turn gravity back on:
            hitObjectRigidBody.useGravity = true;

            //stop objects rotation by setting
            //it to zero to make sure it fires forward
            hitObjectRigidBody.rotation = Quaternion.Euler(Vector3.zero);

            //set direction based on camera transform:
            Vector3 pushDirection = cam.transform.forward;

            //add upwards multiplier only to Y transform
            pushDirection.y *= upMultiplier;

            //Fire the object away
            hitObjectRigidBody.AddRelativeForce(
                pushDirection * throwSpeed, ForceMode.Impulse);

            //reset hand position
            playerHand.transform.localPosition = Vector3.zero;

            //we are no longer holding the object
            isHoldingObject = false;

            //toggle interact button
            input.isInteracting = false;
        }
    }
Rigidbody Based Pick Up and Throw

Rigidbody Based Pick Up and Throw

As the Rigidbody version of the Player continues to develop (full functionality breakdown to follow) the concept of being able to pick up and throw interactable objects now has some solutions.

(Its also worth noting here that the prototype illustrated also has a change color on raycast hit function that I’ll cover elsewhere.)

This added functionality has been on the dev list for a while so its good to get a working prototype up and running.

Development and Code

The basic pseudo code looks something like this:

if isInteracting && !isHoldingObject
check the raycast hit object is Interactable
move the object to the player

isHoldingObject = true
toggle isInteracting

if IsInteracting && isHoldingObject
AddForce to rigidbody based on PlayerCamera transform

isHoldingObject = false
toggle isInteracting

That seems simple enough but I quickly ran into issues specifically around getting more used to handling rigidbodies in FixedUpdate().

This answer on Unity forum was a great help:
https://answers.unity.com/questions/1650650/throwing-object-with-force.html

But before the specifics, one obvious issue with my pseudo code was the move function would put the selected object on top the player – so firstly I needed to set up an offset that would also act as a target to which any selected object could be moved.

Easiest way to do that was to create an Empty +10u on player.z

  • Player hand is the target for any selected objects
  • Its a child of the player camera in order to inherit position and rotation that equates to the ‘look’ direction/rotation of the player. Used to set throw direction.
  • has Trigger that detects when the selected object has reached the ‘hold’ position
  • Layer is set to ‘Ignore Raycast’ (a built in Layer in Unity) that allows the raycast from the player to ignore this object
  • Attached script is used for OntriggerEnter mointoring

Once this was set up I was able to set up and work on the PickAndThrow script attached directly to the Player Object. After a few days of some serious wrestling with this script, below is a working prototype with plenty of commentary that explains the process step by step:

public class PickUpandThrow_PlayerRB : MonoBehaviour
{
   //Get references.....

    private void Start()
    {
        input = GetComponent<InputManager>();
        playerManager = GetComponent<PlayerManager>();
        cam = GetComponentInChildren<Camera>();
        audioSource = GetComponentInChildren<AudioSource>();

        playerHand = GameObject.Find("PlayerHand").transform;
        playerHand_CollisionDetector =
            FindObjectOfType<PlayerHand_CollisionDetector>();
    }

    private void FixedUpdate()
    {
        //Check player hand object for collision with hit RB:
        hitObjectIsAtHand = playerHand_CollisionDetector.hitInteractable;

        if (input.isInteracting && !isHoldingObject)
        {
            //we can pick up the object if we can see it:
            if (playerManager.rayCastHit != null
                && playerManager.rayCastHit.CompareTag("Interactable"))
            {
                //set the RB of target object received from the raycast
                //in PlayerManager:
                hitObjectRigidBody =
                   playerManager.rayCastHit.GetComponent<Rigidbody>();

                //toggle interact button
                input.isInteracting = false;

                //we are now holding the object
                isHoldingObject = true;

                //play audio pick up FX:
                //This clip needs to be adjusted based on difference between
                //hand and object (* pitch)
                audioSource.PlayOneShot(audioClip[0]);
            }
        }

        //Pick up the object
        if (isHoldingObject && !hitObjectIsAtHand)
        {
            //make isKinematic to cancel physics interactions:
            hitObjectRigidBody.isKinematic = true;

            //correct orientation ready for throwing
            hitObjectRigidBody.MoveRotation(Quaternion.Euler(Vector3.zero));

            //move towards player hand
            hitObjectRigidBody.MovePosition(Vector3.MoveTowards(
                hitObjectRigidBody.position,
                playerHand.position,
                pickUpSpeed * Time.fixedDeltaTime));
        }


        if (isHoldingObject && hitObjectIsAtHand)
        {
            //if object has reached player hand, disable its movement
            //by 'teleporting' it into position. 
            hitObjectRigidBody.MovePosition(playerHand.position);
        }

        if (input.isInteracting && isHoldingObject)
        {
            //at this point the object is being thrown so re-enable
            //all physics properties by setting isKinematic = false:
            hitObjectRigidBody.isKinematic = false;

            //Fire the object away (forward)
            //Add force relative to camera in order to take account
            //of rotation.x ('look up')
            hitObjectRigidBody.AddRelativeForce(
                (cam.transform.forward * throwSpeedForward)
                + (cam.transform.up * throwSpeedUp), ForceMode.Impulse);

            //we are no longer holding the object
            isHoldingObject = false;

            //toggle interact button
            input.isInteracting = false;

            audioSource.PlayOneShot(audioClip[1]);
        }
    }
}

One of the main issues here was getting the selected object to Move to the PlayerHand position. Thsi was a lack of understanding of how MovePosition) works – If the rigidbody has isKinematic set to false, RB.MovePosition works like transform.position=newPosition and ‘teleports’ the object to the new position, rather than performing a smooth transition over FixedTime.

The other referenced script of interest here is the PlayerHandCollisionDetector attached (quite logically) to the PlayerHand:

public class PlayerHand_CollisionDetector : MonoBehaviour
{
    PickUpandThrow_PlayerRB pickUpandThrow;
    public bool hitInteractable;

    public void Start()
    {
        //Get the RB from pickUpAndThrow NOT playerManager!!!!
        pickUpandThrow = FindObjectOfType<PickUpandThrow_PlayerRB>();
    }

    private void OnTriggerEnter(Collider collider)
    {
        if (pickUpandThrow.hitObjectRigidBody != null)
        {
            //Check if the incoming collider is the SAME as the stored
            //rigidbody from pickUpandThrow - the RB is constant as its
            //NOT being updated by the raycast.

            //This comparison will avoid false reading from the bool that arose
            //from using the rayCastHit object from playerManager
            if (collider.transform == pickUpandThrow.hitObjectRigidBody.transform)
            {
                //toggle the hit  - BECAUSE THIS IS A TRIGGER!!!!!
                hitInteractable = !hitInteractable;
            }
        }
    }
}

Its easily seen some of the issues I had here! Just making the point that the rigidbody we need to compare is derived from the PickUpandThrow class as the reference there is stable. As opposed to getting the reference from the PlayerManager where the reference is being updated by the raycast.

Summing up

While this setup is functional enough there are some issues that need to be ironed out:

Kinematic Objects don’t care about your colliders!!!
  • selected object can move through other colliders when being ‘carried’ by the player. This due to rigidbody.isKinematic set to true on selection. A solution that comes to mind is to create another empty child of the Player with a box collider that dynamically scales to cover the area of Player + Object. This collider can be activated when hitObjectIsAtHand ?
  • thrown objects sometimes go in unexpected/inaccurate directions. This is due to the AddForce function of the rigiidbody being calculated using cam.transformDirection * a hard coded value, instead of calculating the up value based on camera.rotation.x
  • the standalone nature of this element of the player will be addressed by refactoring and using it as a called method from PlayerManger. In this way, PlayerManager makes conditional calls to other classes which ‘should’ help with efficiency.