[SOLVED] Picking Up/Holding Objects Portal Style

The Aim

As the title suggests, I’m pretty much trying to recreate Portal’s object interaction with regard to picking up and holding objects. The interactable objects each have a collider and a rigidbody. I’ve managed to get the basics down, but I can’t quite get it exactly how I want. The basic summary of what I want is as follows:

&nbsp

  • Toggle the holding of objects with a single button press
  • While held, the object should remain centred in the player camera viewport
  • While held, the object’s position should remain stationary relative to the player
  • While held, the object should not pass through level geometry such as walls and floors
  • While held, the object should react to collisions, but not gravity
  • While not held, the object should react to gravity and collisions

###The Code###
The following code is assigned to the player. It uses a raycast to check for an interactable object, and then calls the object’s interact function when a button is pressed (it’s a little sloppy while I play around with things; open to feedback and advice):

&nbsp

private void Update() {
    if (Input.GetButtonDown("Fire1") && m_CanInteract == true) {
        Interaction();
    }
}


private void FixedUpdate() {
    Ray ray = new Ray(m_CameraTransform.position, m_CameraTransform.forward);
 
    if (Physics.Raycast(ray, out m_RaycastFocus, 3) && m_RaycastFocus.collider.transform.tag == "Interactable") {
        m_CursorImage.color = Color.green;
        m_CanInteract = true;
    }
    else {
        m_CursorImage.color = Color.white;
        m_CanInteract = false;
    }
}


private void Interaction() {
    IInteractable interactComponent = m_RaycastFocus.collider.transform.GetComponent<IInteractable>();

    if (interactComponent != null) {
        interactComponent.Interact();
    }
}

&nbsp

And this code is found in the interactable object’s script. It simply toggles whether or not the object is held, and parent’s the object’s transform to the player camera if it is:

&nbsp

public void Interact() {
    if (!held) {
	    held = true;
        transform.parent = cameraTransform;
    }
    else if (held) {
        held = false;
        transform.parent = null;
    }
}

###The Attempts###
I’ve been playing around with various properties and lines of code but not been able to achieve the effect I’m after. Here are some of my attempts and their faults:

&nbsp

Using only the code above, with default rigidbody component

Held object drops to the floor and does not follow the camera’s viewport accurately.

&nbsp

While held, rigidbody.useGravity = false

Held object does not follow the camera viewport accurately, and floats away when collided with another object.

&nbsp

While held, rigidbody.isKinematic = true

This is almost exactly what I’m after, until the held object is moved towards a wall or floor, at which point the object phases straight through.

&nbsp

While held, rigidbody.constraints = RigidbodyConstraints.FreezeAll

Basically the same as above, except the object now doesn’t rotate with the player (as rotation is frozen). Still phases through level geometry.

&nbsp

No rigidbody component

Mostly just a test, as I still want objects to be affected by physics when not held, but this did work well. The object moves with the player exactly as intended, and cannot pass through walls and floors. However, moving the object into a collision affects the player’s position (e.g. looking down whilst holding the object moves the player on top of it), it would be preferable if the object’s collisions did not affect the player’s rotation and position. Also, no rigidbody isn’t really feasible for my game anyway.


Apologies, this turned out wordier than expected! I’ve tried to be as specific as possible but if there’s any more info needed I’ll be happy to provide. Thanks in advance for any help, and thanks if you even just took the time to read through! :slight_smile:

I’m now a small step closer to my aim. I’ve added a “Player Hands” object which is a child of the player camera. This object has a kinematic Rigidbody and a Fixed Joint component. When an object is picked up by the player, its useGravity property is disabled and it becomes connected to the Fixed Joint. The object now follows the player camera viewport and collides with static colliders.

&nbsp

Sadly, the collision using this method is very erratic and pushing the held object against a wall or floor causes it to jump about rapidly. I’ve tried also parenting the object to the Player Hands object, and I’ve tried the different Collision Detection properties with no success.

&nbsp

I think the joint component might be the solution. Maybe a tense Spring Joint which will accurately follow the camera viewport and close the distance between it and the player when pushed against a collider, coiling back to it’s original position when it has room to do so. Joint components are a new venture for me so I’m not quite there yet.


###The Solution###
Apologies for bumping this thread multiple times, but I’ve come up with a solution I’m satisfied with and I figured I’d share it for anyone stumbling upon this post in the future.

&nbsp

I was spending too much time getting this how I wanted so I came up with a bit of a shortcut method that resolves multiple possible roadblocks. Basically the held object will now be dropped if a large force is acted upon it; such as pushing the object against a wall or floor. I’ll share my own personal scripts so they can be copied and tweaked by whoever needs them. This first script is attached to my player object:

&nbsp

public class PlayerController : MonoBehaviour {
	[SerializeField] private Transform m_CameraTransform = null;
	public Transform m_HandTransform = null;
	[SerializeField] private Image m_CursorImage = null;
    public float m_ThrowForce = 200f;

	private RaycastHit m_RaycastFocus;
	private bool m_CanInteract = false;


	private void Start() {
		m_CameraTransform = GetComponentInChildren<Camera>().transform;
	}

	private void Update() {
		// Has interact button been pressed whilst interactable object is in front of player?
		if (Input.GetButtonDown("Fire1") && m_CanInteract == true) {
			IInteractable interactComponent = m_RaycastFocus.collider.transform.GetComponent<IInteractable>();

			if (interactComponent != null) {
				// Perform object's interaction
				interactComponent.Interact(this);
			}
		}

		// Has action button been pressed whilst interactable object is in front of player?
		if (Input.GetButtonDown("Fire3") && m_CanInteract == true) {
			IInteractable interactComponent = m_RaycastFocus.collider.transform.GetComponent<IInteractable>();

			if (interactComponent != null) {
				// Perform object's action
				interactComponent.Action(this);
			}
		}
	}

	private void FixedUpdate() {
		Ray ray = new Ray(m_CameraTransform.position, m_CameraTransform.forward);

		// Is interactable object detected in front of player?
		if (Physics.Raycast(ray, out m_RaycastFocus, 3) && m_RaycastFocus.collider.transform.tag == "Interactable") {
			m_CursorImage.color = Color.green;
			m_CanInteract = true;
		}
		else {
			m_CursorImage.color = Color.white;
			m_CanInteract = false;
		}
	}
}

&nbsp

And the next script is attached to the object which can be picked up. It uses an IInteractable interface which simply contains void Interact(playerController script) and void Action(playerController script) functions.

&nbsp

public class PhysicsObject : MonoBehaviour, IInteractable {
	public bool m_Held = false;

	private Rigidbody m_ThisRigidbody = null;
	private FixedJoint m_HoldJoint = null;


	private void Start() {
		gameObject.tag = "Interactable";
		m_ThisRigidbody = GetComponent<Rigidbody>();
	}

	private void Update() {
		// If the holding joint has broken, drop the object
		if (m_HoldJoint == null && m_Held == true) {
			m_Held = false;
			m_ThisRigidbody.useGravity = true;
		}
	}

	// Pick up the object, or drop it if it is already being held
	public void Interact(PlayerController playerScript) {
		// Is the object currently being held?
		if (m_Held) {
			Drop();
		}
		else {
			m_Held = true;
			m_ThisRigidbody.useGravity = false;

			m_HoldJoint = playerScript.m_HandTransform.gameObject.AddComponent<FixedJoint>();
			m_HoldJoint.breakForce = 10000f; // Play with this value
			m_HoldJoint.connectedBody = m_ThisRigidbody;
		}
	}

	// Throw the object
	public void Action(PlayerController playerScript) {
		// Is the object currently being held?
		if (m_Held) {
			Drop();

			// Force the object away in the opposite direction of the player
			Vector3 forceDir = transform.position - playerScript.m_HandTransform.position;
			m_ThisRigidbody.AddForce(forceDir * playerScript.m_ThrowForce);
		}
	}

	// Drop the object
	private void Drop() {
		m_Held = false;
		m_ThisRigidbody.useGravity = true;

		Destroy(m_HoldJoint);
	}
} 

&nbsp

I’ve butchered my own scripts a little bit to focus the above on the topic in question, so apologies if I made an error anywhere. Also worth pointing out that I’m using a ‘Player’ GameObject, with the main camera attached as a child, and a ‘Hands’ object with a Rigidbody attached to the camera as a child.

&nbsp

Hopefully I’ve been as clear as possible, and it’s a fairly basic script anyway, but if anyone has any difficulties just leave a comment and I’ll try to help out :slight_smile:

Unity is giving me an issue I can’t solve. What is IInteractable? I can’t find anything on google about it… @Soilyman