Using ValueChanged event with custom ISource

I’m trying to connect the Input System to the Localization system (to show input prompts), elaborating
@karl_jones example from: Localisation for Input Keys

I’ve made some headway with this, but currently I’m stuck on how to process the IVariableValueChanged.ValueChanged event within my custom ISource. I am detecting when the input device changes and firing the event on my IVariables, but there are no subscribers to that event. Is it because of my custom implementation of ISource?

Even though I’m adding the IVariable to the FormatCache.VariableTriggers.

I guess I don’t understand the localization framework good enough to know how my code ties into
LocalizedString.UpdateVariableListeners.

Should I not be building a custom ISource at all? In that case, should I somehow extend PersistentVariablesSource?

Here’s a gist with the essential files (essentially an elaboration of the code you posted above)

The system only subscribes to variables that it is using in the Smart String. So adding it to the local variables is not enough, you need to actually use the source in the smart string.

I think this may be simpler if you implemented it as a variable group instead of creating a new ISource.
I would use IVariableGroup and then add that as a group to the GlobalVariables source.

https://docs.unity3d.com/Packages/com.unity.localization@1.3/manual/Smart/Persistent-Variables-Source.html#custom-variable-groups

You should be able to reuse most of your code. Just make a few tweaks.

1 Like

That would then be a ScriptableObject implementing IVariableGroup?

Yes, that I can do!

1 Like

Yes, I think this will keep things a bit simpler. You will only need 1 class instead of 3. Depending on how you want to update you can then just have a single IVariableValueChanged on the group which will trigger an update for any input changes, or you could return a nested IVariableValueChanged from the group which could then be used to trigger updates for that input only.

1 Like

“You will only need 1 class instead of 3.” Yeah! That makes a lot of sense. My ISource wasn’t doing much except duplicating functionality anyway.

I feel dumb … how do I “use IVariableGroup and then add that as a group to the GlobalVariables source.”.

PersistentVariablesSource takes a “VariableGroupAsset” and what I have is just an IVariableGroup (in a ScriptableObject Asset).

[EDIT: Okay I think I got it. Haha posted simultaneously.]

I think I got it? The custom IVariableGroup should not be an Asset. (ScriptableObject).

Instead, it has to be a [Serializable] Class that also implements IVariable. Then it can be added inside the Variables Group Asset as a nested Variable (Group) ?

Arghh looks like you need to inherit from VariablesGroupAsset however the methods are not virtual so you wont be able to implement the behavior. :frowning:

Ok, instead add the variable group to your global variables asset. It will mean you need to add an extra value to the smart strings though.
e.g {global.input.left}
Ill look into making those methods virtual in the future.

1 Like

Alright, I got it working! (with the caveat of having to add {global}, but eh)

<3

Thanks so much for the help!

Love the way the Input System and Localisation system are built, they’re extensible but also they feel very at home inside of Unity!

I might post it up after a bit if cleaning. (Especially regarding the way the BindingDisplayString is resolved to a sprite, because currently I have some issues with Keyboard “X” vs Gamepad “X”. I guess I should use the control Path instead of the BindingDisplayString. )

1 Like

I am currently facing a similar problem.

I want to represent the bindings of the InputActions in the localization system. However, I can’t quite follow the described explanations.

What would be the best practice to include the InputActions in the localization system in a newer version of the localization package as either local or global variables?

Described in this thread is using the persistent variables system with a custom variable to handle input
https://docs.unity3d.com/Packages/com.unity.localization@1.4/manual/Smart/Persistent-Variables-Source.html
So create a custom IVariable and inside of GetSourceValue you convert the input name into a localized version.

1 Like

Hey @karl_jones , finally took time to pour this into a neat little package that does the plumbing between the Input System and the Localization System:

https://github.com/noio/games.noio.input-hints

Very nice! Thanks for sharing :smile:

@karl_jones I’m trying to implement similar thing with your LiteralTextSource and custom formatter.

[DisplayName("Input Action Formatter")]
public class InputActionFormatter : FormatterBase
{
    private const string SpriteAssetName = "InputHints";

    public override string[] DefaultNames => new[] { "input" };

    public override bool TryEvaluateFormat(IFormattingInfo formattingInfo)
    {
        if (formattingInfo.CurrentValue is string actionName)
        {
            InputAction action = GetAction(actionName);
            string displayString = action.GetBindingDisplayString(group: "Gamepad");
            Debug.Log(displayString);
            formattingInfo.Write($"<sprite=\"{SpriteAssetName}\" name=\"{displayString}\">");
            return true;
        }
        return false;
    }

    public InputAction GetAction(string actionName)
    {
        return InputSystem.actions.FindAction(actionName);
    }
}

With this i can just write something like Press {"Attack":input()} to attack
But I’m not sure how to notify this formatter when input device or binding is changed so it can update itself with new values. Maybe with custom variable I can update formatter with current device but I don’t know how to use literal text and variable at the same time. Do you have any thoughts how this can be implemented?

You could add an event to the FormatCache.

Something like this:

[DisplayName("Input Action Formatter")]
public class InputActionFormatter : FormatterBase, IVariableValueChanged
{
    private const string SpriteAssetName = "InputHints";

    public event Action<IVariable> ValueChanged;

    public override string[] DefaultNames => new[] { "input" };

    public override bool TryEvaluateFormat(IFormattingInfo formattingInfo)
    {
        if (formattingInfo.CurrentValue is string actionName)
        {
            InputAction action = GetAction(actionName);
            string displayString = action.GetBindingDisplayString(group: "Gamepad");
            Debug.Log(displayString);
            formattingInfo.Write($"<sprite=\"{SpriteAssetName}\" name=\"{displayString}\">");

            // Add the callback for when we change
            formattingInfo.FormatDetails.FormatCache.VariableTriggers.Add(this);

            return true;
        }
        return false;
    }

    public void TriggerChange()
    {
        ValueChanged?.Invoke(this);
    }

    public InputAction GetAction(string actionName)
    {
        return InputSystem.actions.FindAction(actionName);
    }

    public object GetSourceValue(ISelectorInfo selector)
    {
        // Not used
        throw new NotImplementedException();
    }
}
1 Like

How do I call TriggerChange()?

Something like LocalizationSettings.StringDatabase.SmartFormatter.GetFormatterExtension<InputActionFormatter>().TriggerChange();

1 Like

Thank you

1 Like