I did try that out with the dummy interactable being parented to the actual interactable, with it on a different interaction layer as you suggested. So the actual interactable is not interactable from the controllers directly but the dummy is, it seems to be forwarding on as you had expected but yeah the repositioning of the proxy interactors and stuff wasn’t very clear to me how to accomplish that.
Yeah I noticed that the there is a lot of logic in the transformer around scaling and stuff. I found that the logic for transforming the motion two handed gun handling is fairly straight forward and you probably understand why this is the case better than I do but when I am using the interactor’s attachTransform for the up when calculating the Quaternion.LookRotation I get the exact same problem where rolling the wrist causes it to get out of wack with the controller’s up, but if I just use the interactor’s up it works the way I want it to. I’ve been trying to write a custom transformer that does exactly this but I haven’t been able to so far.
For now what I have is a modernization of this video:
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.XR.Interaction.Toolkit;
using UnityEngine.XR.Interaction.Toolkit.Interactables;
using UnityEngine.XR.Interaction.Toolkit.Interactors;
public class MultipleInteractorsGrabInteractable : XRGrabInteractable {
[SerializeField]
List<XRSimpleInteractable> extraGrabPoints = new List<XRSimpleInteractable>();
IXRSelectInteractor alternateInteractor;
Quaternion initialAttachRotation;
[SerializeField]
bool snapToSecondHand = true;
Quaternion initialOffsetRotation;
public enum TwoHandRotationType { None, First, Second }
[SerializeField]
TwoHandRotationType rotationType = TwoHandRotationType.First;
private void Start() {
foreach (var grabPoint in extraGrabPoints) {
grabPoint.selectEntered.AddListener(OnSecondHandGrab);
grabPoint.selectExited.AddListener(OnSecondHandRelease);
}
}
public override void ProcessInteractable(XRInteractionUpdateOrder.UpdatePhase updatePhase) {
if (alternateInteractor != null && interactorsSelecting.Count > 0) {
if (snapToSecondHand) {
interactorsSelecting[0].GetAttachTransform(this).rotation = GetTwoHandRotation();
} else {
interactorsSelecting[0].GetAttachTransform(this).rotation = GetTwoHandRotation() * initialOffsetRotation;
}
}
base.ProcessInteractable(updatePhase);
}
protected override void OnSelectEntered(SelectEnterEventArgs args) {
base.OnSelectEntered(args);
alternateInteractor = null;
initialAttachRotation = interactorsSelecting[0].GetAttachTransform(this).localRotation;
}
protected override void OnSelectExited(SelectExitEventArgs args) {
//This doesn't seem needed anymore
//Debug.Log($"select exited interactorsSelecting.Count = {interactorsSelecting.Count}");
//if (interactorsSelecting.Count > 0) {
// interactorsSelecting[0].GetAttachTransform(this).localRotation = initialAttachRotation;
//}
base.OnSelectExited(args);
}
private void OnSecondHandGrab(SelectEnterEventArgs args) {
alternateInteractor = args.interactorObject;
initialOffsetRotation = Quaternion.Inverse(GetTwoHandRotation()) * interactorsSelecting[0].GetAttachTransform(this).rotation;
}
private void OnSecondHandRelease(SelectExitEventArgs args) {
alternateInteractor = null;
}
public override bool IsSelectableBy(IXRSelectInteractor interactor) {
bool isAlreadyGrabbed = interactorsSelecting.Count > 0 && !interactorsSelecting.Contains(interactor);
return base.IsSelectableBy(interactor) && !isAlreadyGrabbed;
}
Quaternion GetTwoHandRotation() {
Quaternion targetRotation = default;
if (rotationType == TwoHandRotationType.None) {
targetRotation = Quaternion.LookRotation(alternateInteractor.transform.position - interactorsSelecting[0].transform.position);
} else if (rotationType == TwoHandRotationType.First) {
targetRotation = Quaternion.LookRotation(alternateInteractor.transform.position - interactorsSelecting[0].transform.position, interactorsSelecting[0].transform.up);
} else if (rotationType == TwoHandRotationType.Second) {
targetRotation = Quaternion.LookRotation(alternateInteractor.transform.position - interactorsSelecting[0].transform.position, alternateInteractor.transform.up);
}
return targetRotation;
}
}
The main issues with doing it this way is it seems to be overriding the transformer system or bypassing it maybe, not really sure. One benefit of this approach is that the weapon is only being selected by the handle interactor and that means the gun can’t be controlled by the off hand basically no matter what, this can be seen as a negative thing though if there were for example something you wanted your offhand to interact with the weapon but not your primary hand, like if you had a flashlight at the end of the gun you wanted to turn on, but an easy circumvention for that is just to have the flashlight be a separate interactable… At any rate, the other issue is it means that when the main hand releases it drops the whole thing. Along with not resetting the aim to the one hand when releasing the second hand. I’m sure it’s solvable but I’m pretty sure this isn’t the route I want to go down. Thinking on it now this is kind of similar to the proxy interactors a little bit. I could foreseeably set up another XRSimpleInteractor for the actual handle and then they would essentially be the proxies for the actual interactable. Then I just need to get rid of the processInteractable here and replace it with a custom gun transformer that does the same stuff. It’s still… lacking a little though.
Here’s my other attempt which thus far has gotten the closest but still needs a custom gun transformer:
Attach Points can be primary or not
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.XR.Interaction.Toolkit;
using UnityEngine.XR.Interaction.Toolkit.Interactables;
using UnityEngine.XR.Interaction.Toolkit.Interactors;
public class GrabInteractableWithGrabPoints : XRGrabInteractable {
[SerializeField]
List<GrabPoint> grabPoints = new List<GrabPoint>();
protected override void OnSelectExited(SelectExitEventArgs args) {
Debug.Log($"Select Exited {args.interactorObject} {args.interactableObject}");
GrabPointTracker tracker = args.interactorObject.transform.GetComponentInParent<GrabPointTracker>();
tracker.GrabPoint = null;
base.OnSelectExited(args);
}
protected override void InitializeDynamicAttachPose(IXRSelectInteractor interactor, Transform dynamicAttachTransform) {
//base.InitializeDynamicAttachPose(interactor, dynamicAttachTransform);
var interactorAttachTransform = interactor.GetAttachTransform(this);
GrabPointTracker tracker = interactor.transform.GetComponentInParent<GrabPointTracker>();
var position = interactorAttachTransform.position;
var rotation = interactorAttachTransform.rotation;
GrabPoint closestGrabPoint = GetClosestGrabPoint(position);
if (closestGrabPoint != null) {
position = closestGrabPoint.transform.position;
if (closestGrabPoint.IsPrimaryAttachPoint) {
rotation = closestGrabPoint.transform.rotation;
}
tracker.GrabPoint = closestGrabPoint;
}
dynamicAttachTransform.SetPositionAndRotation(position, rotation);
}
GrabPoint GetClosestGrabPoint(Vector3 interactorPosition) {
GrabPoint closestGrabPoint = null;
float distance = float.MaxValue;
foreach (var grabPoint in grabPoints) {
var currentDistance = (grabPoint.transform.position - interactorPosition).sqrMagnitude;
if (currentDistance < distance) {
closestGrabPoint = grabPoint;
distance = currentDistance;
}
}
return closestGrabPoint;
}
}
using UnityEngine;
public class GrabPoint : MonoBehaviour {
[SerializeField]
private bool isPrimaryAttachPoint = false;
public bool IsPrimaryAttachPoint { get => isPrimaryAttachPoint; set => isPrimaryAttachPoint = value; }
}
This way works extremely close to what I want where I can grab the gun with either hand, which locks onto the handle (if the handle is closest to the interactor, I want to change it so it’s where the near-far interactor cast hits on the interactable but as it works now is also basically fine). Then when grabbing with the second hand near the foregrip it works as expected. Releasing the foregrip reorients the weapon to the handle attach point aiming which is desireable, regrabbing the foregrip and releasing the handle allows normal manipulation of the gun as if you were holding only the foregrip like turning it and angling it however you feel fit, then regrabbing the handle makes it point where it is supposed to (except for the upward direction issue mentioned before). The only issue is if you do release and regrab the handle, again the order of interactors is incorrect, and also releasing the handle would result in being able to fire the gun with the foregrip hand which I admit will require some more effort in telling the controllers what they are allowed to do based on the attached grabpoint.
Another separate issue that is kind of related to my original workaround of release and regrab for the foregrip hand is that when I do that manually, like if I do the action of releasing and regrabbing the foregrip it doesn’t actually seem to reposition correctly, I can end up with my attachTransform being pretty far away from the actual foregrip transform.
Side NOTE: I’m only using GetComponentInParent temporarily, if I continue down this path I’ll make that available through subclassing.
I feel like it all comes back to not being able to control the order of interactors technically, I haven’t tried it yet because it says in the docs that the interactorsSelecting is read only, but I am wondering if I manually swapped the order to the one I wanted based on if they were the primary attach point or not. I’m sure it would just break a bunch of logic to try that.
I tried using a few other things like the XRDualGrabFreeTransformer and such but I’ve had some pretty weird results with it. I’ve had weird results all over the place trying out different things here and there like when I’m writing my own transformer…