- Home /
[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:
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):
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();
}
}
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:
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:
Using only the code above, with default rigidbody component
Held object drops to the floor and does not follow the camera's viewport accurately.
While held, rigidbody.useGravity = false
Held object does not follow the camera viewport accurately, and floats away when collided with another object.
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.
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.
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! :)
Answer by Soilyman · Jan 26, 2018 at 10:43 AM
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.
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.
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.
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:
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;
}
}
}
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.
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);
}
}
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.
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 :)
This is awesome and it works, but it also makes player to be able to float on the item. Best fix is to add:
Physics.IgnoreCollision(playerScript.GetComponent<Collider>(), GetComponent<Collider>());
into Interact
and Physics.IgnoreCollision(playerScript.GetComponent<Collider>(), GetComponent<Collider>(), false);
into drops.
Answer by timmyiscool8 · Mar 12, 2019 at 06:28 PM
Unity is giving me an issue I can't solve. What is IInteractable? I can't find anything on google about it... @Soilyman
IInteractable interface ... simply contains void
Interact(playerController script)
andvoid Action(playerController script)
functions.
I do that and get Class 'PhysicsObject' cannot have multiple base classes: '$$anonymous$$onoBehaviour' and 'IInteractable'
Your answer
Follow this Question
Related Questions
Method for grabbing rigidbody 2 Answers
How to keep objects from passing through colliders 2 Answers
Pick Up Object using Rigidbody FPS 0 Answers
Pick up object 0 Answers
Prevent a specific RB to influence another, but still collide with everything 0 Answers