Skip to content

Commit

Permalink
NEW: Can set/get long and short display names on controls.
Browse files Browse the repository at this point in the history
  • Loading branch information
Rene Damm committed Nov 12, 2018
1 parent 3fd9844 commit 58e91e5
Show file tree
Hide file tree
Showing 30 changed files with 358 additions and 140 deletions.
18 changes: 18 additions & 0 deletions Assets/Demo/DemoGame.cs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@ public static bool vrSupported

public void Awake()
{
InputUser.onChange += OnUserChange;

// In single player games we want to know when the player switches to a device
// that isn't among the ones currently assigned to the player so that we can
// detect when to switch to a different control scheme.
Expand All @@ -119,6 +121,7 @@ public void Awake()

public void OnDestroy()
{
InputUser.onChange -= OnUserChange;
InputUser.onUnassignedDeviceUsed -= OnUnassignedInputDeviceUsed;
}

Expand Down Expand Up @@ -329,6 +332,21 @@ private void OnUnassignedInputDeviceUsed(IInputUser user, InputAction action, In
.AndAssignMissingDevices();
}

/// <summary>
/// Called when there's a change in the input user setup in the system.
/// </summary>
/// <param name="user"></param>
/// <param name="change"></param>
private void OnUserChange(IInputUser user, InputUserChange change)
{
var player = user as DemoPlayerController;
if (player == null)
return;

if (change == InputUserChange.DevicesChanged)
player.OnAssignedDevicesChanged();
}

/// <summary>
/// Called when a player selects the "Exit" menu item in the player's own menu.
/// </summary>
Expand Down
108 changes: 90 additions & 18 deletions Assets/Demo/DemoPlayerController.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Experimental.Input;
using UnityEngine.Experimental.Input.Interactions;
using UnityEngine.Experimental.Input.Plugins.UI;
using UnityEngine.Experimental.Input.Plugins.Users;
using UnityEngine.Experimental.Input.Utilities;
using UnityEngine.UI;
using Random = UnityEngine.Random;

Expand Down Expand Up @@ -34,27 +36,36 @@ public class DemoPlayerController : MonoBehaviour, IInputUser, IGameplayActions
public DemoControls controls;

/// <summary>
/// UI specific to the player.
/// UI input module specific to the player.
/// </summary>
/// <remarks>
/// We feed input from <see cref="controls"/> into this UI thus making the UI responsive
/// We feed input from <see cref="controls"/> into this module thus making the player's UI responsive
/// to the player's devices only.
/// </remarks>
public Canvas ui;
public UIActionInputModule uiActions;

/// <summary>
/// GameObject hierarchy inside <see cref="ui"/> that represents the menu UI.
/// </summary>
[Tooltip("Root object the per-player menu UI.")]
public GameObject menuUI;

/// <summary>
/// GameObject hierarchy inside <see cref="ui"/> that represents the in-game UI.
/// </summary>
[Tooltip("Root object of the per-player in-game UI.")]
public GameObject inGameUI;

public Text fireHintsUI;
public Text moveHintsUI;
public Text lookHintsUI;
/// <summary>
/// In-game UI that displays control hints.
/// </summary>
[Tooltip("In-game UI to display control hints.")]
public Text controlHintsUI;

/// <summary>
/// In-game UI to show while the player is charging the fire button.
/// </summary>
[Tooltip("In-game UI to show while the player is charging the fire button.")]
public GameObject chargingUI;

public Action<DemoPlayerController> onLeaveGame;
Expand Down Expand Up @@ -84,7 +95,7 @@ public bool isInMenu

public void Start()
{
Debug.Assert(ui != null);
Debug.Assert(uiActions != null);
Debug.Assert(projectilePrefab != null);
Debug.Assert(controls != null);

Expand Down Expand Up @@ -117,10 +128,8 @@ public void PerformOneTimeInitialization(bool isFirstPlayer)
//
// NOTE: Our bindings will be effective on the devices assigned to the user which in turn
// means that the UI will react only to input from that same user.
var uiInput = ui.GetComponent<UIActionInputModule>();
Debug.Assert(uiInput != null);
uiInput.move = new InputActionProperty(controls.menu.navigate);
uiInput.leftClick = new InputActionProperty(controls.menu.click);
uiActions.move = new InputActionProperty(controls.menu.navigate);
uiActions.leftClick = new InputActionProperty(controls.menu.click);
}

/// <summary>
Expand Down Expand Up @@ -348,21 +357,18 @@ public void OnSteamEnterMenu(InputAction.CallbackContext context)
}

/// <summary>
/// Called when the user switches to a different control scheme.
/// Called when the set of devices assigned the player has changed.
/// </summary>
/// <remarks>
/// Updates UI help texts with information based on the bindings in the currently
/// active control scheme. This makes sure we display relevant information in the UI
/// (e.g. gamepad hints instead of keyboard hints when the user is playing with a
/// gamepad).
/// </remarks>
public void OnControlSchemeChanged()
{
//cache UI hints per device
}

public void OnDevicesChanged()
public void OnAssignedDevicesChanged()
{
var devices = this.GetAssignedInputDevices();
controlHintsUI.text = GetOrCreateUIHint(controls.gameplay.fire, "Tap {0} to fire, hold to charge", devices);
}

public void OnCollisionStay()
Expand Down Expand Up @@ -420,4 +426,70 @@ private void FireProjectile()
newProjectile.GetComponent<MeshRenderer>().material.color =
new Color(Random.value, Random.value, Random.value, 1.0f);
}

////TODO: flush out cached UI hints when a device is removed (for good)

private struct CachedUIHint
{
public InputAction action;
public InputDevice device;
public string format;
public string text;
}

private static List<CachedUIHint> s_CachedUIHints;

public static void ClearUIHintsCache()
{
if (s_CachedUIHints != null)
s_CachedUIHints.Clear();
}

/// <summary>
/// Create a textual hint to show for the given action based on the devices we are currently using.
/// </summary>
/// <param name="action">Action to generate a hint for.</param>
/// <param name="format">Format string. Use {0} where the active control name should be inserted.</param>
/// <param name="devices">Set of currently assigned devices. The action will be searched for a bound control that sits
/// on one of the devices. If none is found, an empty string is returned.</param>
/// <returns>Text containing a hint for the given action or an empty string.</returns>
private static string GetOrCreateUIHint(InputAction action, string format, ReadOnlyArray<InputDevice> devices)
{
InputControl control = null;
InputDevice device = null;

// Find the first control that is bound to any of the given devices.
var controls = action.controls;
foreach (var element in controls)
if (devices.ContainsReference(element.device))
{
control = element;
device = control.device;
break;
}

if (control == null)
return string.Empty;

// See if we have an existing hint.
if (s_CachedUIHints != null)
{
foreach (var hint in s_CachedUIHints)
{
if (hint.action == action && hint.device == device && hint.format == format)
return hint.text;
}
}

// No, so create a new hint and cache it.
var controlName = control.shortDisplayName;
if (string.IsNullOrEmpty(controlName))
controlName = control.displayName;
var text = string.Format(format, controlName);
if (s_CachedUIHints == null)
s_CachedUIHints = new List<CachedUIHint>();
s_CachedUIHints.Add(new CachedUIHint {action = action, device = device, format = format, text = text});

return text;
}
}
10 changes: 6 additions & 4 deletions Assets/Demo/Player.prefab
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,8 @@ MonoBehaviour:
moveDeadzone: 0.6
repeatDelay: 0.5
repeatRate: 0.1
trackedDeviceDragThresholdMultiplier: 2
m_SendEventsWhenInBackground: 0
m_PointAction:
m_UseReference: 0
m_Action:
Expand Down Expand Up @@ -392,6 +394,8 @@ MonoBehaviour:
m_Id:
m_SingletonActionBindings: []
m_Reference: {fileID: 0}
m_Touches: []
m_TrackedDevices: []
--- !u!114 &114113743397152476
MonoBehaviour:
m_ObjectHideFlags: 1
Expand Down Expand Up @@ -662,12 +666,10 @@ MonoBehaviour:
type: 2}
controls:
m_Asset: {fileID: 11400000, guid: e0f46a37bbc4f4bf8ad42ad37bb6d56a, type: 3}
ui: {fileID: 223924828555847734}
uiActions: {fileID: 114100797824358962}
menuUI: {fileID: 1592809613891828}
inGameUI: {fileID: 1714024106017884}
fireHintsUI: {fileID: 0}
moveHintsUI: {fileID: 0}
lookHintsUI: {fileID: 0}
controlHintsUI: {fileID: 114282366507092218}
chargingUI: {fileID: 0}
--- !u!114 &114936776529988524
MonoBehaviour:
Expand Down
2 changes: 2 additions & 0 deletions Assets/Tests/DemoGameTestFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,8 @@ public void TearDown()
game.players.Each(UnityEngine.Object.DestroyImmediate);
UnityEngine.Object.DestroyImmediate(game.gameObject);

DemoPlayerController.ClearUIHintsCache();

input.TearDown();

game = null;
Expand Down
12 changes: 7 additions & 5 deletions Assets/Tests/DemoGameTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,22 @@ public partial class DemoGameTests : DemoGameTestFixture
[Property("Device", "Mouse")]
[Property("Device", "Gamepad")]
[Property("Device", "Gamepad")]
[Ignore("TODO")]
public void TODO_Demo_ShowsUIHintsAccordingToCurrentControlScheme()
public void Demo_ShowsUIHintsAccordingToCurrentControlScheme()
{
Click("SinglePlayerButton");

Assert.That(player1.GetControlScheme(), Is.EqualTo(player1.controls.GamepadScheme));
Assert.That(GO<Text>("ControlsHint").text, Is.EqualTo("Tap A button to fire, hold to charge."));
Assert.That(GO<Text>("ControlsHint").text, Is.EqualTo("Tap A to fire, hold to charge"));

Press(mouse.leftButton);

////REVIEW: should we display a different hint while charging?
////REVIEW: should we display a different hint while charging? maybe just display "Charging..." instead of
//// having a dedicated charging UI?

Assert.That(player1.GetControlScheme(), Is.EqualTo(player1.controls.KeyboardMouseScheme));
Assert.That(GO<Text>("ControlsHint").text, Is.EqualTo("Tap LMB to fire, hold to charge."));
Assert.That(GO<Text>("ControlsHint").text, Is.EqualTo("Tap LMB to fire, hold to charge"));

////TODO: switch back and make sure we're not allocating GC memory
}

[Test]
Expand Down
3 changes: 2 additions & 1 deletion Assets/Tests/DemoGameTests_SinglePlayer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ public void TODO_Demo_SinglePlayer_CanBringUpInGameMenu()
Click("SinglePlayerButton");
Trigger("gameplay/menu");

Assert.That(game.players[0].ui.enabled, Is.True);
Assert.That(game.players[0].menuUI.activeSelf, Is.True);
Assert.That(game.players[0].inGameUI.activeSelf, Is.False);
Assert.That(game.players[0].controls.gameplay.enabled, Is.False);
Assert.That(game.players[0].controls.menu.enabled, Is.True);
Assert.That(game.mainMenuCanvas.enabled, Is.False);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -662,6 +662,7 @@ private void SetUpPerActionCachedBindingData()
}
}

////TODO: re-use allocations such that only grow the arrays and hit zero GC allocs when we already have enough memory
internal void ClearPerActionCachedBindingData()
{
m_BindingsForEachAction = null;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System;
using System.Net.NetworkInformation;
using UnityEngine.Experimental.Input.Utilities;

////TODO: support for removing bindings
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,25 +28,25 @@ public enum ButtonBits
/// <summary>
/// The button representing the vertical upwards state of the D-Pad.
/// </summary>
[InputControl(bit = (int)ButtonBits.Up)]
[InputControl(bit = (int)ButtonBits.Up, displayName = "Up", shortDisplayName = "\u2191")]
public ButtonControl up { get; private set; }

/// <summary>
/// The button representing the vertical downwards state of the D-Pad.
/// </summary>
[InputControl(bit = (int)ButtonBits.Down)]
[InputControl(bit = (int)ButtonBits.Down, displayName = "Down", shortDisplayName = "\u2193")]
public ButtonControl down { get; private set; }

/// <summary>
/// The button representing the horizontal left state of the D-Pad.
/// </summary>
[InputControl(bit = (int)ButtonBits.Left)]
[InputControl(bit = (int)ButtonBits.Left, displayName = "Left", shortDisplayName = "\u2190")]
public ButtonControl left { get; private set; }

/// <summary>
/// The button representing the horizontal right state of the D-Pad.
/// </summary>
[InputControl(bit = (int)ButtonBits.Right)]
[InputControl(bit = (int)ButtonBits.Right, displayName = "Right", shortDisplayName = "\u2192")]
public ButtonControl right { get; private set; }

////TODO: should have X and Y child controls as well
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,12 @@ public string name
/// a display name there, the display name will default to <see cref="name"/>. However, specific
/// controls may override this behavior. <see cref="KeyControl"/>, for example, will set the
/// display name to the actual key name corresponding to the current keyboard layout.
///
/// For nested controls, the display name will include the display names of all parent controls,
/// i.e. the display name will fully identify the control on the device. For example, the display
/// name for the left D-Pad button on a gamepad is "D-Pad Left" and not just "Left".
/// </remarks>
/// <seealso cref="shortDisplayName"/>
public string displayName
{
get
Expand All @@ -103,6 +108,34 @@ public string displayName
protected set { m_DisplayName = value; }
}

/// <summary>
/// An alternate, abbreviated <see cref="displayName"/> (for example "LMB" instead of "Left Button").
/// </summary>
/// <remarks>
/// If the control has no abbreviated version, this will be null. Note that this behavior is different
/// from <see cref="displayName"/> which will fall back to <see cref="name"/> if not display name has
/// been assigned to the control.
///
/// For nested controls, the short display name will include the short display names of all parent controls,
/// i.e. the display name will fully identify the control on the device. For example, the display
/// name for the left D-Pad button on a gamepad is "D-Pad Left" and not just "Left". Note that if a parent
/// control has no short name, its long name will be used instead.
/// </remarks>
/// <seealso cref="displayName"/>
public string shortDisplayName
{
get
{
RefreshConfigurationIfNeeded();
if (m_ShortDisplayName != null)
return m_ShortDisplayName;
if (m_ShortDisplayNameFromLayout != null)
return m_ShortDisplayNameFromLayout;
return null;
}
protected set { m_ShortDisplayName = value; }
}

/// <summary>
/// Full path all the way from the root.
/// </summary>
Expand Down Expand Up @@ -381,6 +414,8 @@ protected internal uint stateOffsetRelativeToDeviceRoot
internal string m_Path;
internal string m_DisplayName; // Display name set by the control itself (may be null).
internal string m_DisplayNameFromLayout; // Display name coming from layout (may be null).
internal string m_ShortDisplayName; // Short display name set by the control itself (may be null).
internal string m_ShortDisplayNameFromLayout; // Short display name coming from layout (may be null).
internal InternedString m_Layout;
internal InternedString m_Variants;
internal InputDevice m_Device;
Expand Down
Loading

0 comments on commit 58e91e5

Please sign in to comment.