/*
* Author: mark
* Date: 8/12/2024
* Description: interaction for snapping objects to a target, and vice versa
*/
using System;
using UnityEngine;
using UnityEngine.XR.Interaction.Toolkit.Interactables;
// WARN: this is an incredibly delicate bit of bodge code, here lies dragons
///
/// class for snapping objects to a target, and vice versa
///
public class Snapping : MonoBehaviour
{
///
/// enum to determine what type of snapping behaviour the object should have
///
public enum Type
{
Target,
NonTarget
}
///
/// enum to determine the snapping behaviour of the object
///
[SerializeField] public Type snappingBehaviourType = Type.NonTarget;
///
/// optional tag to match if we are a target
///
[SerializeField] private string optionalMatchIfTag;
///
/// bool to keep the object permanently snapped to the target
///
[SerializeField] private bool keepObjectSnapped;
///
/// public bool for other scripts to check if an object has snapped to this object, if it is a target
///
public bool hasObjectSnapped;
///
/// flag to wait until the next frame to prevent multiple OnTriggerEnter calls
///
private bool _blockOnTriggerEnter;
///
/// flag to wait until the next frame to prevent multiple OnTriggerExit calls
///
private bool _blockOnTriggerExit;
///
/// interactable to re-enable after we have snapped
///
private Transform _incomingSnappedTransform;
///
/// interactable to keep track of if they leave the snapping trigger
///
private Transform _outgoingSnappedTransform;
///
/// first-ran function to check if the snapping prefab or this component is set up correctly
///
///
private void Awake()
{
// check if we have a trigger collider as a target
if (snappingBehaviourType != Type.Target) return;
var triggerCollider = GetComponent();
if (triggerCollider == null)
throw new Exception("this snapping object is a target, yet does not have a trigger collider. "
+ "this should not happen, as triggers are used by targets to detect when a non-target is colliding with it!");
}
///
/// update state of any variables
///
private void Start()
{
if (snappingBehaviourType == Type.NonTarget) return;
// if we are a target...
foreach (Transform child in transform)
{
// hide any children objects
var possibleMeshRenderer = child.GetComponent();
if (possibleMeshRenderer != null) possibleMeshRenderer.enabled = false;
// and also any children colliders
var possibleCollider = child.GetComponent();
if (possibleCollider != null) possibleCollider.enabled = false;
}
}
///
/// check if we need to re-enable any grabbable objects
///
private void Update()
{
// do not for the love of god remove or even move this line within the function
if (_blockOnTriggerEnter) _outgoingSnappedTransform = null;
_blockOnTriggerEnter = false;
_blockOnTriggerExit = false;
// if we have a grabbed object, snap it and re-enable it (because we must've just snapped to it)
// ReSharper disable once InvertIf
if (_incomingSnappedTransform != null)
{
Snap(_incomingSnappedTransform);
// one more time, just to be sure
_incomingSnappedTransform.position = transform.position;
_incomingSnappedTransform.rotation = transform.rotation;
// then reenable if needed
if (!keepObjectSnapped) _incomingSnappedTransform.GetComponent().enabled = true;
// ...and go about our merry way
_incomingSnappedTransform = null;
}
if (hasObjectSnapped) Debug.Log("we are snapped!");
}
///
/// snap/teleport non-targets to target objects
///
/// the object entering the trigger
private void OnTriggerEnter(Collider other)
{
if (_blockOnTriggerEnter) return;
Debug.Log("snapping trigger hit");
// if we are a non-target, we should not have a trigger collider
if (snappingBehaviourType != Type.Target)
throw new Exception("this snapping object is a non-target, yet has a trigger. "
+ "this should not happen, as triggers are used by targets to detect when a non-target is colliding with it!");
// check if we are just accidentally reregistering a previously snapped object
if ((_outgoingSnappedTransform != null) & (_outgoingSnappedTransform == other.transform))
{
Debug.Log("...already snapped this one");
_incomingSnappedTransform = null;
_outgoingSnappedTransform = null;
// god, I do not know why these need to be here but they *just* need to be here
// _blockOnTriggerEnter = true;
_blockOnTriggerExit = true;
return;
}
// are we colliding with another object that has a 'Snapping' component?
var possibleSnappingComponent = other.transform.GetComponentsInParent();
if (possibleSnappingComponent == null)
{
Debug.Log("not snapping this one... "
+ "(not in a Snapping prefab or parent game object with the Snapping component)");
return;
}
// if an optional tag string is set, check if the object or parent prefab/game object has that tag
if (!string.IsNullOrEmpty(optionalMatchIfTag))
{
var possibleTag = other.transform.tag ?? "";
var possibleParentTag = other.transform.parent?.tag ?? "";
if (possibleTag != optionalMatchIfTag && possibleParentTag != optionalMatchIfTag)
{
Debug.Log("not snapping this one... "
+ "(does not have the optional tag string)");
return;
}
}
_incomingSnappedTransform = other.transform;
_blockOnTriggerEnter = true;
Stop(other.transform);
}
///
/// reset the snapped interactable object
///
/// the other object exiting the trigger
private void OnTriggerExit(Collider other)
{
if (_blockOnTriggerExit) return;
Debug.Log("snapping trigger exit~");
Stop(other.transform);
_outgoingSnappedTransform = other.transform;
_blockOnTriggerEnter = true;
hasObjectSnapped = false;
}
///
/// function to stop the object from moving
///
/// Transform passed in from OnTrigger physics calls
private void Stop(Transform otherTransform)
{
var possibleRigidbody = otherTransform.GetComponent();
if (possibleRigidbody == null) return;
possibleRigidbody.velocity = Vector3.zero;
possibleRigidbody.angularVelocity = Vector3.zero;
}
///
/// function to snap it so that it happens in update land and not in OnTrigger___ land
///
/// Transform passed in from OnTrigger physics calls
private void Snap(Transform otherTransform)
{
// NOTE: literally do NOT move/inline this function anywhere
// call order and "race conditions" have caused me hours of great pain
// make the user drop it
var possibleInteractable = otherTransform.GetComponent();
if (possibleInteractable != null && possibleInteractable.isSelected)
possibleInteractable.enabled = false;
// then snap it
// Stop(otherTransform);
otherTransform.position = transform.position;
otherTransform.rotation = transform.rotation;
_blockOnTriggerExit = true;
hasObjectSnapped = true;
Debug.Log("snapped");
}
}