Implement a mouse drag composite !

I was looking for a 'mouse drag' action,

So ... it was bit of a pain but apparently I have something that works !

So we can all understand:

  • Action Type should be Value ?
  • Control Type should be Vector2 ?
  • but what about Interactions, Processors, should they be left off ?

One of the problem that I have currently is Canceled is being emitted when I'm not moving anymore while mouse button is still being held.

Do Unity team has plans for this ?

Else, can someone at Unity help getting this proper ?

Thanks :)

using System.Diagnostics.CodeAnalysis;
using JetBrains.Annotations;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.InputSystem.Layouts;
#if UNITY_EDITOR
using UnityEditor;

#endif

namespace z
{
#if UNITY_EDITOR
    [InitializeOnLoad]
#endif
    public class MouseDragComposite : InputBindingComposite<Vector2>
    {
        static MouseDragComposite()
        {
            InputSystem.RegisterBindingComposite<MouseDragComposite>();
        }

        [RuntimeInitializeOnLoadMethod]
        [SuppressMessage("Code Quality", "IDE0051:Remove unused private members", Justification = "<Pending>")]
        private static void Init()
        {
        }

        public override Vector2 ReadValue(ref InputBindingCompositeContext context)
        {
            var b = context.ReadValueAsButton(Button);
            var x = context.ReadValue<float>(Axis1);
            var y = context.ReadValue<float>(Axis2);
            var v = new Vector2(x, y);

            return b && v.magnitude > 0.0f ? v : default;
        }

        public override float EvaluateMagnitude(ref InputBindingCompositeContext context)
        {
            return ReadValue(ref context).magnitude;
        }

        #region Fields

        [InputControl(layout = "Button")]
        [UsedImplicitly]
        public int Button;

        [InputControl(layout = "Axis")]
        [UsedImplicitly]
        public int Axis1;

        [InputControl(layout = "Axis")]
        [UsedImplicitly]
        public int Axis2;

        #endregion
    }
}

EDIT

I have been able to get proper interaction:

  • started
  • processed over and over
  • canceled
using System;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.InputSystem.Controls;
#if UNITY_EDITOR
using UnityEditor;

#endif

namespace z
{
    /// <summary>
    ///     Mouse drag interaction.
    /// </summary>
#if UNITY_EDITOR
    [InitializeOnLoad]
#endif
    public class MouseDragInteraction : IInputInteraction
    {
        static MouseDragInteraction()
        {
            InputSystem.RegisterInteraction<MouseDragInteraction>();
        }

        public void Reset()
        {
        }

        public void Process(ref InputInteractionContext context)
        {
            if (context.timerHasExpired)
            {
                context.Performed();
                return;
            }

            var phase = context.phase;

            switch (phase)
            {
                case InputActionPhase.Disabled:
                    break;
                case InputActionPhase.Waiting:
                    if (context.ControlIsActuated())
                    {
                        context.Started();
                        context.SetTimeout(float.PositiveInfinity);
                    }

                    break;
                case InputActionPhase.Started:
                    context.PerformedAndStayPerformed();
                    break;
                case InputActionPhase.Performed:
                    if (context.ControlIsActuated())
                    {
                        context.PerformedAndStayPerformed();
                    }
                    else if (!((ButtonControl) context.action.controls[0]).isPressed)
                    {
                        context.Canceled();
                    }

                    break;
                case InputActionPhase.Canceled:
                    break;
                default:
                    throw new ArgumentOutOfRangeException(nameof(phase), phase, null);
            }
        }

        [RuntimeInitializeOnLoadMethod]
        private static void Init()
        {
        }
    }
}

So no more funny state when debugging the input. :smile:

11 Likes

I have the same issue, mouse drag is a pain to implement, there is no way to define that easily, and once you succeed using UPDATE function to simulate it like you did with the old system, then you have to decide how to distinguish drag vs mouse click.

I can see the beauty and evolution of the new system, but the basic functionality are missing.
Like mouse drag, distinction between a mouse drag and a click on the same button
Keyboard event that are not repeated in callback, so that force you to use update to have a constant drag with keyboard.
Touchpad how are considered as mouse and not as an independent device like touch
Cancel function that is not always call.

The new system show potential, but at least for what I need, I ended up doing everything in update using polling, and almost defining no action, even wondering why I should...

Sure that device connect/disconnect, multi player are nice, but if once connected you can't do basic stuff, what does it server :(

3 Likes

I've edited first post and added the second part of the code I just wrote, the interaction. It works as expected: now there's only single started and cancelled phases.

This is how it should be configured:

Action
Action Type = Pass Through
Control Type = Vector2
Interactions
Mouse Drag
Add Mouse Drag composite

Then use it, in my case I was looking for implementing a panning function so I fetched delta, so far it's working well !

But we still would like an official answer :)

9 Likes

I'm hoping something like this can be built in, too; Essentially, I want to be able to bind an axis that only fires updates for my Action whilst a modifier is being held down (along with start and end events).

(It shouldn't be too mouse-specific, since I'm actually hoping to use this with XR buttons and tracked position axes instead!)

I too think this is a core feature that should come with the package. I just wanted to rotate an object by horizontal mouse movement only during mouse drag. Previously i tracked all of it manually and worked out the phases from mouse click and delta myself, which is fine... but making this stuff easier is kinda the point of actions no?

2 Likes

Gesture support is indeed a missing piece. It is one of the highest priority items for after 1.0. What's there ATM is indeed ill-equipped to do this gracefully. There's ways to route compound pointer input into a higher level recognizes in the way that the Touch Samples do but the input system adds little there other than mapping input sources.

What this will look like exactly is too early to tell. My thoughts (it's really not more than that ATM) are that I'd like to see two things happen:

  • Interactions being extended to where the "recognizer" part splits off and is its own thing. Meaning that instead of an interaction just sitting as its own object on bindings, there's a recognizer that can feed interactions in from outside. Including interactions that have been recognized externally (e.g. by the OS).
  • A set of MonoBehaviours that wrap this nicely such that a reasonable set of "default" interactions can be directly correlated with GameObjects and made contextual on them.
1 Like

What i was hoping for as I'm trying to learn this new system is:

  • configure 1-n conditions that when satisfied will trigger the event to be fired.
  • configure what data gets sent with the event.

So with my example earlier:

Mouse left pressed to start Player/Dragging action. (Started phase)
* Action includes mouse Position.
While held Action fires continually (Active phase)
* Action includes mouse Delta.
Mouse left released (Finished phase)
* Action includes mouse Position.

I was able to get the phases sorted using the script provided earlier in this thread for the custom interaction.
But i couldn't figure out from the Actions Editor how to have a trigger condition on the action that is different than the data to be delivered. What i came up with was using a custom composite. Is there a way to do this is directly in the Editor?

using Unity.Mathematics;
using UnityEditor;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.InputSystem.Utilities;
using UnityEngine.InputSystem.Layouts;
using UnityEngine.Scripting;

public struct MouseDragCompositeResult
{
    public float2 Position;
    public float2 Delta;

    public override string ToString() => $"{{ Position: {Position}, Delta:{Delta} }}";
}

[Preserve]
#if UNITY_EDITOR
[InitializeOnLoad]
#endif
[DisplayStringFormat("{Modifier}+{Delta}+{Position}")]
public class MouseDragComposite : InputBindingComposite<MouseDragCompositeResult>
{
    [RuntimeInitializeOnLoadMethod]
    static void Init() { }

    static MouseDragComposite()
    {
        InputSystem.RegisterBindingComposite<MouseDragComposite>();
    }

    [InputControl(layout = "Button")]
    public int Modifier;

    [InputControl(layout = "Vector2")]
    public int Delta;

    [InputControl(layout = "Vector2")]
    public int Position;

    private Vector2MagnitudeComparer _comparer = new Vector2MagnitudeComparer();

    public override MouseDragCompositeResult ReadValue(ref InputBindingCompositeContext context)
    {
        if (context.ReadValueAsButton(Modifier))
        {
            var delta = context.ReadValue<Vector2, Vector2MagnitudeComparer>(Delta, _comparer);
            var position = context.ReadValue<Vector2, Vector2MagnitudeComparer>(Position, _comparer);

            return new MouseDragCompositeResult
            {
                Delta = delta,
                Position = position,
            };
        }
        return default;
    }

    public override float EvaluateMagnitude(ref InputBindingCompositeContext context)
    {
        var value = ReadValue(ref context);
        return math.length(value.Delta);
    }
}

How did you do it with the delta?

Did you add this to the script the new input system made or created as a different script then add to player?

Hi! Thanks for this solution!
It works pretty well, but I'm seeing 2 issues.
When using only the MouseDragComposition( without the interaction ), the action is still being executed sending a 0 Delta when mouse button is not being pressed.
If I add the MouseDragInteraction then the action is not executed if I not press the mouse button, but when I release it, the action is no executed with a 0 Delta, thus my local values remain with previous values, which are never 0.

Following the examples above I was able to make a functioning mouse drag input. It works fine when launched in a local scene from the editor but in a compiled build it complains that the binding composite has not been registered. Am I missing something?

InvalidOperationException: No binding composite with name 'MouseDrag' has been registered
at UnityEngine.InputSystem.InputBindingResolver.InstantiateBindingComposite (System.String nameAndParameters) [0x00038] in <7cd406f3e9c44161a27643afba86ee8e>:0
at UnityEngine.InputSystem.InputBindingResolver.AddActionMap (UnityEngine.InputSystem.InputActionMap map) [0x00336] in <7cd406f3e9c44161a27643afba86ee8e>:0
UnityEngine.DebugLogHandler:Internal_LogException(Exception, Object)
UnityEngine.DebugLogHandler:LogException(Exception, Object)
UnityEngine.Logger:LogException(Exception, Object)
UnityEngine.Debug:LogException(Exception)
UnityEngine.InputSystem.InputBindingResolver:AddActionMap(InputActionMap)
UnityEngine.InputSystem.InputActionMap:ResolveBindings()
UnityEngine.InputSystem.InputActionMap:ResolveBindingsIfNecessary()
UnityEngine.InputSystem.InputActionMap:Enable()
UnityEngine.InputSystem.PlayerInput:set_currentActionMap(InputActionMap)
UnityEngine.InputSystem.PlayerInput:SwitchCurrentActionMap(String)
UnityEngine.InputSystem.PlayerInput:ActivateInput()
UnityEngine.InputSystem.PlayerInput:OnEnable()

1 Like

So I figured this out and just wanted to share.

By switching all of the use cases of

[RuntimeInitializeOnLoadMethod]

to

[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]

resolved the problem. Might be something specific to my setup that required this.

3 Likes

I'm a tad confused, what is the purpose of the "MouseDragInteraction" Class by aybe?

Isn't drag only useful for UI (such as inventory screen), so since the new Input system doesn't support the IDropHandler/IBeginDraghandler etc. events.

wouldn't you need to use your UI canva's graphic raycaster and simply get the current mouse position from the new input system and then use the objects hit to do the drag/drop interaction in code?

Or am I misunderstanding something?

When setting up like this, I get all events firing repeatedly even when not holding down the mouse. Is this not correctly setup?

7297243--883162--upload_2021-7-4_23-19-10.png

Edit:
Ah, you needed both the composite and the interaction! Thank you for sharing the script :)

Works like a charm, thanks so much. This absolutely needs to be part of the input system by default.

Extremely valuable post! Any news from Unity on when this functionality will be part of the InputSystem by default ? @Rene-Damm

@aybe 's solution works like a charm indeed, if all the properties are set correctly. Here's the setup that works for me:

1.Create the Action using the appropriate Action Type and Control Type, and add a Mouse Drag Interaction (should appear in the list if you have added @aybe 's second script in your project)
7668595--957547--upload_2021-11-18_16-48-43.png

  1. Create a Moude Drag Composite for that Action (should appear in the list if you have added @aybe 's first script in your project)
    7668595--957553--upload_2021-11-18_16-48-55.png

  2. Add the Mouse Drag Interaction to the Composite as well (didn't work without this in my case)
    7668595--957544--upload_2021-11-18_16-47-33.png

  3. Set the button of your choice (in my case Left Button) as the Binding for Button
    7668595--957556--upload_2021-11-18_16-50-6.png

  4. Set the X & Y axes of your choice (either <Mouse>/position/x & <Mouse>/position/y or <Mouse>/delta/x & <Mouse>/delta/y
    7668595--957562--upload_2021-11-18_16-53-19.png

Finally you should be able to read the Vector2 axis whenever a drag is happening, using the method of your choice. Here I am using a callback subscribed to the Drag event of the Player Input Component:

 public void Drag(InputAction.CallbackContext ctx)
    {
        Debug.Log(ctx.ReadValue<Vector2>());
    }
6 Likes

Hey @aybe & @GeorgeAdamon thanks a bunch for this gentlemen, it really shed some light onto my poor InputSystem internals understanding!
@aybe 's custom composite and processor work grear except one thing - i am having an issue where the RadValue only returns whole positive numbers (still 2 values, thats correct), no floats and no negative values at all.
It happens when i use both pointer x/y position and delta.
I have used @aybe 's code as is, without any modifications and I believe I am folowing the setup you suggested.
Did you guys also experience this?

EDIT
It turned out it was because I had the action defined for general pointer to handle mouse and touch input simultaneously (button bound to ponter press, axes bound to pointer xy delta). When I sseparated the action into two, one for mouse and one for touch, it started to behave.
Slick! You guys rock;) Merry Xmass and happy new year!

1 Like

@aybe and @GeorgeAdamon , thank you so much for the great explanation! And it works great :) However I'd like to tweak what you did to allow me to get the mouse drag start position (so I can get the direction from start to current pos and return that value), but I can't seem to find how to do that.

Any clue ? Binding composite being stateless isn't helping...

@tarahugger this is great, any idea how you'd adjust it to prevent events from firing entirely if delta is zero? Is this even something possible?

i cant get this to work with other controls bound to the mouse capture has anyone got this going with multiple other control options.