VR Development Framework
v 1.0.0
 All Classes Namespaces Functions Variables Enumerations Enumerator Properties Events Pages
AVR_Grabbable.cs
1 using System.Collections;
2 using System.Collections.Generic;
3 using UnityEngine;
4 using System.Linq;
5 
6 namespace AVR.Phys {
7  /// <summary>
8  /// Represents a grabbable object.
9  /// </summary>
10  [AVR.Core.Attributes.DocumentationUrl("class_a_v_r_1_1_phys_1_1_a_v_r___grabbable.html")]
11  public class AVR_Grabbable : AVR.Core.AVR_Component
12  {
13  /// <summary>
14  /// Type that describes the objects behaviour when grabbed.
15  /// </summary>
16  public GrabbableObjectType objectType;
17  private GrabbableObjectType _objType;
18 
19  /// <summary>
20  /// List of GrabNodes that are attatched to this object
21  /// </summary>
22  public List<AVR_GrabNode> grabNodes => nodes;
23  private readonly List<AVR_GrabNode> nodes = new List<AVR_GrabNode>();
24 
25  /// <summary>
26  /// Rigidbody of this grabbable object.
27  /// </summary>
28  public Rigidbody rb;
29 
30  /// <summary>
31  /// Optional AudioSource to play sounds from GrabbableObjectType data
32  /// </summary>
33  public AudioSource source;
34 
35  /// <summary>
36  /// List of colliders that describe the outer/grabbable surface. All colliders must be convex.
37  /// </summary>
38  public List<Collider> colliders = new List<Collider>();
39 
40  [HideInInspector]
41  /// <summary>
42  /// List of hands that are currently grabbing this object.
43  /// </summary>
44  public List<AVR_BasicGrabProvider> AttachedHands = new List<AVR_BasicGrabProvider>();
45 
46  private Transform old_parent;
47 
48  private List<Vector3> velocities = new List<Vector3>();
49  const int velocity_count = 3;
50 
51  private float last_dist = 0.0f;
52 
53  /// <summary>
54  /// True if the object is being grabbed, otherwise false.
55  /// </summary>
56  public bool isGrabbed {
57  get { return AttachedHands.Count>0; }
58  }
59 
60  /// <summary>
61  /// True if the object is being grabbed by 2 or more hands, otherwise false.
62  /// </summary>
63  public bool isGrabbedByMultipleHands {
64  get { return AttachedHands.Count>1; }
65  }
66 
67  void Reset() {
68  #if AVR_NET
69  this.destroyOnRemote = false;
70  #endif
71  }
72 
73  protected override void Awake()
74  {
75  base.Awake();
76  if (rb == null) rb = GetComponent<Rigidbody>();
77  if (colliders==null || colliders.Count<1) colliders.AddRange(GetComponentsInChildren<Collider>());
78  if (nodes == null || nodes.Count < 1) nodes.AddRange(GetComponentsInChildren<AVR_GrabNode>());
79  if (objectType == null) objectType = GrabbableObjectType.defaultObjectType();
80  if (source == null) source = GetComponent<AudioSource>();
81 
82  //Throw warnings if audiosource isnt a 3D blending source
83  if (source != null)
84  {
85  if (source.spatialBlend == 0.0f)
86  {
87  Debug.Log(this.name +
88  "'s Audio Source has no spatial blend. 3D audio will not work. Consider setting spatial blend to 1");
89  }
90  }
91  }
92 
93  void FixedUpdate()
94  {
95  if (isGrabbed)
96  {
97  UpdateRBVelocity();
98  }
99  else {
100  force = Vector3.zero;
101  }
102  }
103 
104  public void Grab(AVR_BasicGrabProvider hand)
105  {
106  #if AVR_NET
107  // If we are online and this object is not owned by the grabbing player or the server -> dont allow grab.
108  if(IsOnline && !IsOwnedByServer && !IsOwner)
109  {
110  return;
111  }
112  #endif
113 
114  if(!isGrabbed) {
115  old_parent = this.transform.parent;
116  // while its being grabbed, set the rig as its parent, so that when the rig teleport we dont break the grab-joint.
117  transform.SetParent(AVR.Core.AVR_PlayerRig.Instance.transform);
118  }
119  else if(isGrabbed && !_objType.allowTwoHanded) {
120  // flush AttatchedHands.
121  while(AttachedHands.Count>0) AttachedHands[0].makeRelease();
122  }
123  AttachedHands.Add(hand);
124  hand.controller.HapticPulse(0.3f, 0.05f);
125 
126  //Play pickup sound
127  if (source != null && objectType.soundData.pickupSound != null)
128  {
129  source.PlayOneShot(objectType.soundData.pickupSound, objectType.soundData.volumeMultiplier);
130  }
131 
132 #if AVR_NET
133  if (IsOnline)
134  {
135  NetworkObject.ChangeOwnership(this.OwnerClientId);
136  }
137 #endif
138  }
139 
140  public void Release(AVR_BasicGrabProvider hand)
141  {
142  #if AVR_NET
143  if (IsOnline) NetworkObject.RemoveOwnership();
144  #endif
145 
146  if(AttachedHands.Contains(hand)) AttachedHands.Remove(hand);
147  if(AttachedHands.Count<1) {
148  transform.SetParent(old_parent);
149  if(velocities.Count>0) rb.velocity = new Vector3(velocities.Average(v => v.x), velocities.Average(v => v.y), velocities.Average(v => v.z));
150  velocities.Clear();
151 
152  //Play release sound
153  if (source != null && objectType.soundData.releaseSound != null)
154  {
155  source.PlayOneShot(objectType.soundData.releaseSound, objectType.soundData.volumeMultiplier);
156  }
157  }
158  }
159 
160  Vector3 lastVel = Vector3.zero;
161  Vector3 wacc = Vector3.zero;
162  Vector3 worldvel = Vector3.zero;
163 
164  Vector3 force = Vector3.zero;
165  Vector3 cvel = Vector3.zero;
166 
168  {
169  #if AVR_NET
170  if(IsOnline && !IsOwner) return;
171  #endif
172 
173  // Get pos-rot of hand and item
174  Vector3 targetItemPosition = transform.position;
175  Quaternion targetItemRotation = transform.rotation;
176  Vector3 targetHandPosition = this.getTargetPosition();
177  Quaternion targetHandRotation = this.getTargetRotation();
178 
179  // Set the current objecttype to the regular object type or the nested one
180  _objType = (isGrabbedByMultipleHands && objectType.changeObjectTypeOnTwoHanded) ? objectType.typeOnTwoHanded : objectType;
181 
182  // Break joint if the distance is too far & it is growing.
183  if (Vector3.Distance(targetItemPosition, targetHandPosition) > last_dist && last_dist > _objType.Break_grab_distance)
184  {
185  while (AttachedHands.Count > 0) AttachedHands[0].makeRelease();
186  return;
187  }
188  last_dist = Vector3.Distance(targetItemPosition, targetHandPosition);
189 
190  // Follow aglorithms
191  switch(_objType.followType) {
192  case GrabbableObjectType.FollowType.FREE : {
193  //pos
194  Vector3 pDelta = (targetHandPosition - targetItemPosition);
195  Vector3 vel = pDelta / Time.fixedDeltaTime;
196 
197  rb.velocity = vel * _objType.Lightness;
198  lastVel = rb.velocity;
199 
200  //ang
201  Quaternion rotationDelta = targetHandRotation * Quaternion.Inverse(targetItemRotation);
202 
203  rotationDelta.ToAngleAxis(out float angle, out Vector3 axis);
204  while (angle > 180) angle -= 360; // Prevent object from doing sudden >180° turns instead of negative <180° ones
205 
206  rb.maxAngularVelocity = 99999.0f;
207 
208  Vector3 angvel = (angle * axis * Mathf.Deg2Rad) / Time.fixedDeltaTime;
209  if (!float.IsNaN(angvel.z)) rb.angularVelocity = angvel * _objType.Angular_Lightness;
210  break;
211  }
212  case GrabbableObjectType.FollowType.STATIC : {
213  break;
214  }
215  case GrabbableObjectType.FollowType.CONSTRAINED : {
216  Vector3 pDelta = (targetHandPosition - targetItemPosition);
217  Vector3 vel = pDelta / Time.fixedDeltaTime;
218 
219  // The current world acceleration is rb.velocity - lastVel. However, due to sudden changes in the objects position, we prevent the acceleration from jumping to
220  // dramatically by smoothing out the world acceleration throuhg a simple linear interpolation
221  // Lower the value of 0.5 to make the world acceleration more delayed/elastic, but also lower the jitter an object may experience
222  wacc = Vector3.Lerp(wacc, rb.velocity - lastVel, 0.5f);
223 
224  worldvel = wacc / Time.fixedDeltaTime; //We scale wacc similarly to pDelta, to compare with vel
225 
226  worldvel -= worldvel.normalized * Mathf.Min(worldvel.magnitude, 10); //10 == minimum force applied by hand
227  vel = Vector3.ClampMagnitude(vel, 30); //30 == maximum force applied by hand
228 
229  if (!float.IsNaN(worldvel.x) && !float.IsNaN(worldvel.y) && !float.IsNaN(worldvel.z))
230  {
231  rb.velocity = vel + worldvel;
232  }
233  else
234  {
235  rb.velocity = Vector3.zero;
236  }
237 
238  lastVel = rb.velocity;
239 
240 
241  //ang
242  Quaternion rotationDelta = targetHandRotation * Quaternion.Inverse(targetItemRotation);
243 
244  rotationDelta.ToAngleAxis(out float angle, out Vector3 axis);
245  while (angle > 180) angle -= 360; // Prevent object from doing sudden >180° turns instead of negative <180° ones
246 
247  rb.maxAngularVelocity = 99999.0f;
248 
249  Vector3 angvel = (angle * axis * Mathf.Deg2Rad) / Time.fixedDeltaTime;
250  if (!float.IsNaN(angvel.z)) rb.angularVelocity = (angvel * objectType.Angular_Lightness);
251  break;
252  }
253  case GrabbableObjectType.FollowType.HEAVY : {
254  // Point where object was grabbed
255  Vector3 closestp = AttachedHands[0].getWorldGrabLocation();
256 
257  // Difference between point where object was grabbed at and current hand/palm position (normalized over a few frames)
258  force = Vector3.Lerp(force, AttachedHands[0].grabPoint.position - closestp, 0.25f);
259 
260  // Velocity of rigidbody at grabbed position (normalized)
261  cvel = Vector3.Lerp(cvel, rb.GetPointVelocity(closestp), 0.25f);
262 
263  float k = 0.3f;
264  // delta = the force we want to apply minus the objects current velocity
265  Vector3 delta = force - Vector3.Lerp(Vector3.zero, cvel, k * Vector3.Distance(AttachedHands[0].grabPoint.position, closestp));
266  Vector3 f = delta * Time.fixedDeltaTime * 100.0f * _objType.Heavy_force_multiplier;
267 
268  rb.velocity *= 0.8f;
269  rb.AddForceAtPosition(f, closestp, ForceMode.Impulse);
270  break;
271  }
272  }
273  velocities.Add(rb.velocity);
274  while (velocities.Count > velocity_count) velocities.RemoveAt(0);
275  }
276 
277  Vector3 getTargetPosition() {
278  if(!isGrabbed) {
279  return transform.position;
280  }
281  else if(AttachedHands.Count == 1) {
282  return AttachedHands[0].getTargetPosition();
283  }
284  else {
285  Vector3 sum = Vector3.zero;
286  foreach (AVR_BasicGrabProvider h in AttachedHands) sum += h.getTargetPosition();
287  return sum/AttachedHands.Count;
288  }
289  }
290 
291  Quaternion getTargetRotation() {
292  if (!isGrabbed) {
293  return transform.rotation;
294  }
295  else if (AttachedHands.Count == 1) {
296  return AttachedHands[0].getTargetRotation();
297  }
298  else {
299  // TODO: This sometimes leads to weird rotations.
300  // This is the simplest way to get the average of multiple quaternions:
301  float w = 1f / AttachedHands.Count;
302  Quaternion avg = Quaternion.identity;
303  for (int i = 0; i < AttachedHands.Count; i++)
304  {
305  Quaternion q = AttachedHands[i].getTargetRotation();
306  avg *= Quaternion.Slerp(Quaternion.identity, q, w);
307  }
308  return avg;
309  }
310  }
311 
312  void OnCollisionEnter(Collision collision)
313  {
314  // Haptic feedback
315  foreach(AVR_BasicGrabProvider h in AttachedHands)
316  {
317  h.controller.HapticPulse(Mathf.Lerp(0, 0.5f, 0.2f * collision.relativeVelocity.magnitude), 0.05f);
318  }
319 
320  //Play collision sound
321  if (source != null && objectType.soundData.collideSound != null && rb != null)
322  {
323  //Calculate a sound multiplier based off the velocity and mass of the collision
324  float soundMultiplier = objectType.soundData.volumeMultiplier;
325 
326  float mass = rb.mass;
327  float speed = rb.velocity.magnitude;
328  float drag = rb.drag == 0.0f ? 0.5f : rb.drag; //If the rigidbody drag is 0, use 0.5. Otherwise, use that drag.
329 
330  const float frontalArea = 0.5f;
331  const float airDrag = 1.1f;
332 
333  //Equation adapted and modified from https://www.grc.nasa.gov/WWW/K-12/rocket/termvr.html
334  float terminalSpeed = Mathf.Sqrt(2.0f * mass * Physics.gravity.magnitude / airDrag * frontalArea * drag);
335 
336  //When you grab an object, it teleports.
337  //This creates these wildly large speed values when grabbed. This will ignore them.
338  if (speed > terminalSpeed)
339  {
340  return;
341  }
342 
343  //Using the adapted terminal velocity, create a ratio from 0 - 1 of speed.
344  //Use this ratio as the sound multiplier. The faster the object moves, the louder the sound.
345  float physicsSoundMultiplier = speed / terminalSpeed;
346 
347  soundMultiplier *= physicsSoundMultiplier;
348 
349  source.PlayOneShot(objectType.soundData.collideSound, soundMultiplier);
350  }
351  }
352  }
353 }
AudioSource source
Optional AudioSource to play sounds from GrabbableObjectType data
override void Awake()
Defines how an object behaves whilst grabbed.
Represents a grabbable object.
Quaternion getTargetRotation()
Rigidbody rb
Rigidbody of this grabbable object.
Sets the documentation html file inside of Packages/com.avr.core/Documentation/html of a given class...
Simplest GrabProvider. Grabbed objects will move their center (obj.transform.position) towards the re...
void OnCollisionEnter(Collision collision)
void Release(AVR_BasicGrabProvider hand)
void Grab(AVR_BasicGrabProvider hand)