Implementing pinching

How I understand Input System actions and bindings

  • Action supposed to be client-action, which is abstraction higher then reading inputs. It is game (application) events like “Jump” or “Shoot”.
  • Bindings is just reading inputs in all ways you can achieve it.

What I want to achieve: implement cross platform camera Zoom action. For mouse device it is simple as adding Mouse/Scroll/Y binding with Clamp(-1, 1) processor, so after that I can “Zoom” anything depending on -1, 0, 1 values.
With touchscreen device it is much harder. There is some tutorials from devs where they create actions like “Touch”, “Drag”, etc. But as I’ve mentioned in 1st section I don’t want to use actions in that way. So I’ve started to search a way to create suitable binding.

So I’ve found two possibilities

  • Implement custom processor which will triggered by any Touch #0/Delta or Touch #0/Position (no matter what indeed, because we just want to trigger processor here). Then in Process method access to actual controls through Touchscreen.current.TryGetChildControl<TouchControl>("/touch0"); (same for /touch1) and then read current position / start position / touch phase. It is working solution but it looks kinda hacky.
  • Implement custom composite binding. It looks like more straight way to implement such things, because in examples when we want to read values from mouse but only when left button is holding we can use composite with one modifier and set modifier to something like /leftButton. But while implementing composite for pinching I see no proper way to make conditional input (source code for OneModifierComposite.cs consist of some under the hood magic which I don’t understand so I don’t know how it becomes conditional)

How can it be implemented with composites? How to check if particular composite part is performed by now?

What I’ve tried:

  • Check by InputBindingCompositeContext.EvaluateMagnitude(partIndex) and InputBindingCompositeContext.GetPressTime(partIndex) but those values never become default after first actuation
  • Check by InputBindingCompositeContext.ReadAsButton(partIndex) but I get errors InvalidOperationException: Cannot read value of type 'float' from control '/Touchscreen/touch0/position' bound to action

UPD: I’ve solved it by using /touch #0 and /touch #1 + InputBindingCompositeContext.ReadValue<TouchPhase, TouchPhaseComparer>(partIndex) then I can read touch phase / position / start position / etc from solid touch data

Note: while implementing I was getting invalid TouchPhase values without any visible reason. All because of returning 0 in TouchPhaseComparer as compare result, it seems inner code returns some default values when bindings are equal, so returning just 1 instead of 0 solved all problems.

#if UNITY_EDITOR
using System.Collections.Generic;
using Unity.Mathematics;
using UnityEditor;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.InputSystem.Layouts;
using UnityEngine.InputSystem.LowLevel;
using UnityEngine.InputSystem.Utilities;
using TouchPhase = UnityEngine.InputSystem.TouchPhase;

[InitializeOnLoad]
#endif
[DisplayStringFormat("{firstTouch}+{secondTouch}")]
public class PinchingComposite : InputBindingComposite<float>
{
    [InputControl(layout = "Value")]
    public int firstTouch;
    [InputControl(layout = "Value")]
    public int secondTouch;

    public float negativeScale = 1f;
    public float positiveScale = 1f;

    private struct TouchStateComparer : IComparer<TouchState>
    {
        public int Compare(TouchState x, TouchState y) => 1;
    }

    // This method computes the resulting input value of the composite based
    // on the input from its part bindings.
    public override float ReadValue(ref InputBindingCompositeContext context)
    {
        var touch_0 = context.ReadValue<TouchState, TouchStateComparer>(firstTouch);
        var touch_1 = context.ReadValue<TouchState, TouchStateComparer>(secondTouch);

        if (touch_0.phase != TouchPhase.Moved || touch_1.phase != TouchPhase.Moved)
            return 0f;

        var startDistance = math.distance(touch_0.startPosition, touch_1.startPosition);
        var distance = math.distance(touch_0.position, touch_1.position);

        var unscaledValue = startDistance / distance - 1f; // startDistance divide by distance to invert value
        return unscaledValue * (unscaledValue < 0 ? negativeScale : positiveScale);
    }

    // This method computes the current actuation of the binding as a whole.
    public override float EvaluateMagnitude(ref InputBindingCompositeContext context) => 1f;

    static PinchingComposite() => InputSystem.RegisterBindingComposite<PinchingComposite>();

    [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
    static void Init() { } // Trigger static constructor.
}

8635956--1161330--upload_2022-12-5_15-30-3.png

1 Like

Hi Tony,

Sorry to necro this thread when it was marked as resolved but I’m trying use your composite for a project of mine and am running into some issues. namely, touch_0.phase and touch_1.phase always report as none although I am very much using multiple fingers on my phone. Have you run into this issue while getting your composite to work?

I’m not asking you to troubleshoot my issue so if theres nothing off the top of your head that you could point to, I’m more than happy to resolve this myself.

Also thanks for the code you provided and updating a thread purely for people that might stumble across it on their search!

1 Like

I’ve checked project where I had implemented this composite and it looks like code completely same as I provide here. You can check it here. Also you can download project and try to catch the problem.

1 Like

thanks :slight_smile: