This commit is contained in:
2025-01-17 13:10:42 +01:00
commit 4536213c91
15115 changed files with 1442174 additions and 0 deletions

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: a5a01adef66544c688fb903cd7f37b96
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,4 @@
{
"displayName": "Custom Binding Composite",
"description": "Shows how to implement a custom composite binding."
}

View File

@@ -0,0 +1,174 @@
using System;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.InputSystem.Layouts;
using UnityEngine.InputSystem.Utilities;
#if UNITY_EDITOR
using UnityEditor;
using UnityEngine.InputSystem.Editor;
using UnityEngine.UIElements;
#endif
// Let's say we want to have a composite that takes an axis and uses
// it's value to multiply the length of a vector from a stick. This could
// be used, for example, to have the right trigger on the gamepad act as
// a strength multiplier on the value of the left stick.
//
// We start by creating a class that is based on InputBindingComposite<>.
// The type we give it is the type of value that we will compute. In this
// case, we will consume a Vector2 from the stick so that is the type
// of value we return.
//
// NOTE: By advertising the type of value we return, we also allow the
// input system to filter out our composite if it is not applicable
// to a specific type of action. For example, if an action is set
// to "Value" as its type and its "Control Type" is set to "Axis",
// our composite will not be shown as our value type (Vector2) is
// incompatible with the value type of Axis (float).
//
// We can customize the way display strings are formed for our composite by
// annotating it with DisplayStringFormatAttribute. The string is simply a
// list with elements to be replaced enclosed in curly braces. Everything
// outside those will taken verbatim. The fragments inside the curly braces
// in this case refer to the binding composite parts by name. Each such
// instance is replaced with the display text for the corresponding
// part binding.
//
// NOTE: We don't supply a name for the composite here. The default logic
// will take the name of the type ("CustomComposite" in our case)
// and snip off "Composite" if used as a suffix (which is the case
// for us) and then use that as the name. So in our case, we are
// registering a composite called "Custom" here.
//
// If we were to use our composite with the AddCompositeBinding API,
// for example, it would look like this:
//
// myAction.AddCompositeBinding("Custom")
// .With("Stick", "<Gamepad>/leftStick")
// .With("Multiplier", "<Gamepad>/rightTrigger");
[DisplayStringFormat("{multiplier}*{stick}")]
public class CustomComposite : InputBindingComposite<Vector2>
{
// So, we need two parts for our composite. The part that delivers the stick
// value and the part that delivers the axis multiplier. Note that each part
// may be bound to multiple controls. The input system handles that for us
// by giving us an integer identifier for each part that reads a single value
// from however many controls are bound to the part.
//
// In our case, this could be used, for example, to bind the "multiplier" part
// to both the left and the right trigger on the gamepad.
// To tell the input system of a "part" binding that we need for a composite,
// we add a public field with an "int" type and annotated with an [InputControl]
// attribute. We set the "layout" property on the attribute to tell the system
// what kind of control we expect to be bound to the part.
//
// NOTE: These part binding need to be *public fields* for the input system
// to find them.
//
// So this is introduces a part to the composite called "multiplier" and
// expecting an "Axis" control. The value of the field will be set by the
// input system. It will be some internal, unique numeric ID for the part
// which we can then use with InputBindingCompositeContext.ReadValue to
// read out the value of just that part.
[InputControl(layout = "Axis")]
public int multiplier;
// The other part we need is for the stick.
//
// NOTE: We could use "Stick" here but "Vector2" is a little less restrictive.
[InputControl(layout = "Vector2")]
public int stick;
// We may also expose "parameters" on our composite. These can be configured
// graphically in the action editor and also through AddCompositeBinding.
//
// Let's say we want to allow the user to specify an additional scale factor
// to apply to the value of "multiplier". We can do so by simply adding a
// public field of type float. Any public field that is not annotated with
// [InputControl] will be treated as a possible parameter.
//
// If we added a composite with AddCompositeBinding, we could configure the
// parameter like so:
//
// myAction.AddCompositeBinding("Custom(scaleFactor=0.5)"
// .With("Multiplier", "<Gamepad>/rightTrigger")
// .With("Stick", "<Gamepad>/leftStick");
public float scaleFactor = 1;
// Ok, so now we have all the configuration in place. The final piece we
// need is the actual logic that reads input from "multiplier" and "stick"
// and computes a final input value.
//
// We can do that by defining a ReadValue method which is the actual workhorse
// for our composite.
public override Vector2 ReadValue(ref InputBindingCompositeContext context)
{
// We read input from the parts we have by simply
// supplying the part IDs that the input system has set up
// for us to ReadValue.
//
// NOTE: Vector2 is a less straightforward than primitive value types
// like int and float. If there are multiple controls bound to the
// "stick" part, we need to tell the input system which one to pick.
// We do so by giving it an IComparer. In this case, we choose
// Vector2MagnitudeComparer to return the Vector2 with the greatest
// length.
var stickValue = context.ReadValue<Vector2, Vector2MagnitudeComparer>(stick);
var multiplierValue = context.ReadValue<float>(multiplier);
// The rest is simple. We just scale the vector we read by the
// multiple from the axis and apply our scale factor.
return stickValue * (multiplierValue * scaleFactor);
}
}
// Our custom composite is complete and fully functional. We could stop here and
// call it a day. However, for the sake of demonstration, let's say we also want
// to customize how the parameters for our composite are edited. We have "scaleFactor"
// so let's say we want to replace the default float inspector with a slider.
//
// We can replace the default UI by simply deriving a custom InputParameterEditor
// for our composite.
#if UNITY_EDITOR
public class CustomCompositeEditor : InputParameterEditor<CustomComposite>
{
public override void OnGUI()
{
// Using the 'target' property, we can access an instance of our composite.
var currentValue = target.scaleFactor;
// The easiest way to lay out our UI is to simply use EditorGUILayout.
// We simply assign the changed value back to the 'target' object. The input
// system will automatically detect a change in value.
target.scaleFactor = EditorGUILayout.Slider(m_ScaleFactorLabel, currentValue, 0, 2);
}
#if UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS
public override void OnDrawVisualElements(VisualElement root, Action onChangedCallback)
{
var slider = new Slider(m_ScaleFactorLabel.text, 0, 2)
{
value = target.scaleFactor,
showInputField = true
};
// Note: For UIToolkit sliders, as of Feb 2022, we can't register for the mouse up event directly
// on the slider because an element inside the slider captures the event. The workaround is to
// register for the event on the slider container. This will be fixed in a future version of
// UIToolkit.
slider.Q("unity-drag-container").RegisterCallback<MouseUpEvent>(evt =>
{
target.scaleFactor = slider.value;
onChangedCallback?.Invoke();
});
root.Add(slider);
}
#endif
private GUIContent m_ScaleFactorLabel = new GUIContent("Scale Factor");
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: af7a21cd3a00840cfb1bcdab751adde7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1 @@
This sample shows how to implement and register a custom InputBindingComposite.

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: fc5a49c66f7a54b89887a931c5ca4d86
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 5e6bd783c06264b3eba95763870116e9
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,4 @@
{
"displayName": "Custom Device",
"description": "Shows how to implement a custom input device."
}

View File

@@ -0,0 +1,352 @@
using System.Linq;
using System.Runtime.InteropServices;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.InputSystem.Controls;
using UnityEngine.InputSystem.Layouts;
using UnityEngine.InputSystem.LowLevel;
using UnityEngine.InputSystem.Utilities;
#if UNITY_EDITOR
using UnityEditor;
#endif
// The input system stores a chunk of memory for each device. What that
// memory looks like we can determine ourselves. The easiest way is to just describe
// it as a struct.
//
// Each chunk of memory is tagged with a "format" identifier in the form
// of a "FourCC" (a 32-bit code comprised of four characters). Using
// IInputStateTypeInfo we allow the system to get to the FourCC specific
// to our struct.
public struct CustomDeviceState : IInputStateTypeInfo
{
// We use "CUST" here as our custom format code. It can be anything really.
// Should be sufficiently unique to identify our memory format, though.
public FourCC format => new FourCC('C', 'U', 'S', 'T');
// Next we just define fields that store the state for our input device.
// The only thing really interesting here is the [InputControl] attributes.
// These automatically attach InputControls to the various memory bits that
// we define.
//
// To get started, let's say that our device has a bitfield of buttons. Each
// bit indicates whether a certain button is pressed or not. For the sake of
// demonstration, let's say our device has 16 possible buttons. So, we define
// a ushort field that contains the state of each possible button on the
// device.
//
// On top of that, we need to tell the input system about each button. Both
// what to call it and where to find it. The "name" property tells the input system
// what to call the control; the "layout" property tells it what type of control
// to create ("Button" in our case); and the "bit" property tells it which bit
// in the bitfield corresponds to the button.
//
// We also tell the input system about "display names" here. These are names
// that get displayed in the UI and such.
[InputControl(name = "firstButton", layout = "Button", bit = 0, displayName = "First Button")]
[InputControl(name = "secondButton", layout = "Button", bit = 1, displayName = "Second Button")]
[InputControl(name = "thirdButton", layout = "Button", bit = 2, displayName = "Third Button")]
public ushort buttons;
// Let's say our device also has a stick. However, the stick isn't stored
// simply as two floats but as two unsigned bytes with the midpoint of each
// axis located at value 127. We can simply define two consecutive byte
// fields to represent the stick and annotate them like so.
//
// First, let's introduce stick control itself. This one is simple. We don't
// yet worry about X and Y individually as the stick as whole will itself read the
// component values from those controls.
//
// We need to set "format" here too as InputControlLayout will otherwise try to
// infer the memory format from the field. As we put this attribute on "X", that
// would come out as "BYTE" -- which we don't want. So we set it to "VC2B" (a Vector2
// of bytes).
[InputControl(name = "stick", format = "VC2B", layout = "Stick", displayName = "Main Stick")]
// So that's what we need next. By default, both X and Y on "Stick" are floating-point
// controls so here we need to individually configure them the way they work for our
// stick.
//
// NOTE: We don't mention things as "layout" and such here. The reason is that we are
// modifying a control already defined by "Stick". This means that we only need
// to set the values that are different from what "Stick" stick itself already
// configures. And since "Stick" configures both "X" and "Y" to be "Axis" controls,
// we don't need to worry about that here.
//
// Using "format", we tell the controls how their data is stored. As bytes in our case
// so we use "BYTE" (check the documentation for InputStateBlock for details on that).
//
// NOTE: We don't use "SBYT" (signed byte) here. Our values are not signed. They are
// unsigned. It's just that our "resting" (i.e. mid) point is at 127 and not at 0.
//
// Also, we use "defaultState" to tell the system that in our case, setting the
// memory to all zeroes will *NOT* result in a default value. Instead, if both x and y
// are set to zero, the result will be Vector2(-1,-1).
//
// And then, using the various "normalize" parameters, we tell the input system how to
// deal with the fact that our midpoint is located smack in the middle of our value range.
// Using "normalize" (which is equivalent to "normalize=true") we instruct the control
// to normalize values. Using "normalizeZero=0.5", we tell it that our midpoint is located
// at 0.5 (AxisControl will convert the BYTE value to a [0..1] floating-point value with
// 0=0 and 255=1) and that our lower limit is "normalizeMin=0" and our upper limit is
// "normalizeMax=1". Put another way, it will map [0..1] to [-1..1].
//
// Finally, we also set "offset" here as this is already set by StickControl.X and
// StickControl.Y -- which we inherit. Note that because we're looking at child controls
// of the stick, the offset is relative to the stick, not relative to the beginning
// of the state struct.
[InputControl(name = "stick/x", defaultState = 127, format = "BYTE",
offset = 0,
parameters = "normalize,normalizeMin=0,normalizeMax=1,normalizeZero=0.5")]
public byte x;
[InputControl(name = "stick/y", defaultState = 127, format = "BYTE",
offset = 1,
parameters = "normalize,normalizeMin=0,normalizeMax=1,normalizeZero=0.5")]
// The stick up/down/left/right buttons automatically use the state set up for X
// and Y but they have their own parameters. Thus we need to also sync them to
// the parameter settings we need for our BYTE setup.
// NOTE: This is a shortcoming in the current layout system that cannot yet correctly
// merge parameters. Will be fixed in a future version.
[InputControl(name = "stick/up", parameters = "normalize,normalizeMin=0,normalizeMax=1,normalizeZero=0.5,clamp=2,clampMin=0,clampMax=1")]
[InputControl(name = "stick/down", parameters = "normalize,normalizeMin=0,normalizeMax=1,normalizeZero=0.5,clamp=2,clampMin=-1,clampMax=0,invert")]
[InputControl(name = "stick/left", parameters = "normalize,normalizeMin=0,normalizeMax=1,normalizeZero=0.5,clamp=2,clampMin=-1,clampMax=0,invert")]
[InputControl(name = "stick/right", parameters = "normalize,normalizeMin=0,normalizeMax=1,normalizeZero=0.5,clamp=2,clampMin=0,clampMax=1")]
public byte y;
}
// Now that we have the state struct all sorted out, we have a way to lay out the memory
// for our device and we have a way to map InputControls to pieces of that memory. What
// we're still missing, however, is a way to represent our device as a whole within the
// input system.
//
// For that, we start with a class derived from InputDevice. We could also base this
// on something like Mouse or Gamepad in case our device is an instance of one of those
// specific types but for this demonstration, let's assume our device is nothing like
// those devices (if we base our devices on those layouts, we have to correctly map the
// controls we inherit from those devices).
//
// Other than deriving from InputDevice, there are two other noteworthy things here.
//
// For one, we want to ensure that the call to InputSystem.RegisterLayout happens as
// part of startup. Doing so ensures that the layout is known to the input system and
// thus appears in the control picker. So we use [InitializeOnLoad] and [RuntimeInitializeOnLoadMethod]
// here to ensure initialization in both the editor and the player.
//
// Also, we use the [InputControlLayout] attribute here. This attribute is optional on
// types that are used as layouts in the input system. In our case, we have to use it
// to tell the input system about the state struct we are using to define the memory
// layout we are using and the controls tied to it.
#if UNITY_EDITOR
[InitializeOnLoad] // Call static class constructor in editor.
#endif
[InputControlLayout(stateType = typeof(CustomDeviceState))]
public class CustomDevice : InputDevice, IInputUpdateCallbackReceiver
{
// [InitializeOnLoad] will ensure this gets called on every domain (re)load
// in the editor.
#if UNITY_EDITOR
static CustomDevice()
{
// Trigger our RegisterLayout code in the editor.
Initialize();
}
#endif
// In the player, [RuntimeInitializeOnLoadMethod] will make sure our
// initialization code gets called during startup.
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
private static void Initialize()
{
// Register our device with the input system. We also register
// a "device matcher" here. These are used when a device is discovered
// by the input system. Each device is described by an InputDeviceDescription
// and an InputDeviceMatcher can be used to match specific properties of such
// a description. See the documentation of InputDeviceMatcher for more
// details.
//
// NOTE: In case your device is more dynamic in nature and cannot have a single
// static layout, there is also the possibility to build layouts on the fly.
// Check out the API documentation for InputSystem.onFindLayoutForDevice and
// for InputSystem.RegisterLayoutBuilder.
InputSystem.RegisterLayout<CustomDevice>(
matches: new InputDeviceMatcher()
.WithInterface("Custom"));
}
// While our device is fully functional at this point, we can refine the API
// for it a little bit. One thing we can do is expose the controls for our
// device directly. While anyone can look up our controls using strings, exposing
// the controls as properties makes it simpler to work with the device in script.
public ButtonControl firstButton { get; protected set; }
public ButtonControl secondButton { get; protected set; }
public ButtonControl thirdButton { get; protected set; }
public StickControl stick { get; protected set; }
// FinishSetup is where our device setup is finalized. Here we can look up
// the controls that have been created.
protected override void FinishSetup()
{
base.FinishSetup();
firstButton = GetChildControl<ButtonControl>("firstButton");
secondButton = GetChildControl<ButtonControl>("secondButton");
thirdButton = GetChildControl<ButtonControl>("thirdButton");
stick = GetChildControl<StickControl>("stick");
}
// We can also expose a '.current' getter equivalent to 'Gamepad.current'.
// Whenever our device receives input, MakeCurrent() is called. So we can
// simply update a '.current' getter based on that.
public static CustomDevice current { get; private set; }
public override void MakeCurrent()
{
base.MakeCurrent();
current = this;
}
// When one of our custom devices is removed, we want to make sure that if
// it is the '.current' device, we null out '.current'.
protected override void OnRemoved()
{
base.OnRemoved();
if (current == this)
current = null;
}
// So, this is all great and nice. But we have one problem. No one is actually
// creating an instance of our device yet. Which means that while we can bind
// to controls on the device from actions all we want, at runtime we will never
// actually receive input from our custom device. For that to happen, we need
// to make sure that an instance of the device is created at some point.
//
// This one's a bit tricky. Because it really depends on how the device is
// actually discovered in practice. In most real-world scenarios, there will be
// some external API that notifies us when a device under its domain is added or
// removed. In response, we would report a device being added (using
// InputSystem.AddDevice(new InputDeviceDescription { ... }) or removed
// (using DeviceRemoveEvent).
//
// In this demonstration, we don't have an external API to query. And we don't
// really have another criteria by which to determine when a device of our custom
// type should be added.
//
// So, let's fake it here. First, to create the device, we simply add a menu entry
// in the editor. Means that in the player, this device will never be functional
// but this serves as a demonstration only anyway.
//
// NOTE: Nothing of the following is necessary if you have a device that is
// detected and sent input for by the Unity runtime itself, i.e. that is
// picked up from the underlying platform APIs by Unity itself. In this
// case, when your device is connected, Unity will automatically report an
// InputDeviceDescription and all you have to do is make sure that the
// InputDeviceMatcher you supply to RegisterLayout matches that description.
//
// Also, IInputUpdateCallbackReceiver and any other manual queuing of input
// is unnecessary in that case as Unity will queue input for the device.
#if UNITY_EDITOR
[MenuItem("Tools/Custom Device Sample/Create Device")]
private static void CreateDevice()
{
// This is the code that you would normally run at the point where
// you discover devices of your custom type.
InputSystem.AddDevice(new InputDeviceDescription
{
interfaceName = "Custom",
product = "Sample Product"
});
}
// For completeness sake, let's also add code to remove one instance of our
// custom device. Note that you can also manually remove the device from
// the input debugger by right-clicking in and selecting "Remove Device".
[MenuItem("Tools/Custom Device Sample/Remove Device")]
private static void RemoveDevice()
{
var customDevice = InputSystem.devices.FirstOrDefault(x => x is CustomDevice);
if (customDevice != null)
InputSystem.RemoveDevice(customDevice);
}
#endif
// So the other part we need is to actually feed input for the device. Notice
// that we already have the IInputUpdateCallbackReceiver interface on our class.
// What this does is to add an OnUpdate method that will automatically be called
// by the input system whenever it updates (actually, it will be called *before*
// it updates, i.e. from the same point that InputSystem.onBeforeUpdate triggers).
//
// Here, we can feed input to our devices.
//
// NOTE: We don't have to do this here. InputSystem.QueueEvent can be called from
// anywhere, including from threads. So if, for example, you have a background
// thread polling input from your device, that's where you can also queue
// its input events.
//
// Again, we don't have actual input to read here. So we just make up some stuff
// here for the sake of demonstration. We just poll the keyboard
//
// NOTE: We poll the keyboard here as part of our OnUpdate. Remember, however,
// that we run our OnUpdate from onBeforeUpdate, i.e. from where keyboard
// input has not yet been processed. This means that our input will always
// be one frame late. Plus, because we are polling the keyboard state here
// on a frame-to-frame basis, we may miss inputs on the keyboard.
//
// NOTE: One thing we could instead is to actually use OnScreenControls that
// represent the controls of our device and then use that to generate
// input from actual human interaction.
public void OnUpdate()
{
var keyboard = Keyboard.current;
if (keyboard == null)
return;
var state = new CustomDeviceState();
state.x = 127;
state.y = 127;
// WARNING: It may be tempting to simply store some state related to updates
// directly on the device. For example, let's say we want scale the
// vector from WASD to a certain length which can be adjusted with
// the scroll wheel of the mouse. It seems natural to just store the
// current strength as a private field on CustomDevice.
//
// This will *NOT* work correctly. *All* input state must be stored
// under the domain of the input system. InputDevices themselves
// cannot private store their own separate state.
//
// What you *can* do however, is simply add fields your state struct
// (CustomDeviceState in our case) that contain the state you want
// to keep. It is not necessary to expose these as InputControls if
// you don't want to.
// Map WASD to stick.
var wPressed = keyboard.wKey.isPressed;
var aPressed = keyboard.aKey.isPressed;
var sPressed = keyboard.sKey.isPressed;
var dPressed = keyboard.dKey.isPressed;
if (aPressed)
state.x -= 127;
if (dPressed)
state.x += 127;
if (wPressed)
state.y += 127;
if (sPressed)
state.y -= 127;
// Map buttons to 1, 2, and 3.
if (keyboard.digit1Key.isPressed)
state.buttons |= 1 << 0;
if (keyboard.digit2Key.isPressed)
state.buttons |= 1 << 1;
if (keyboard.digit3Key.isPressed)
state.buttons |= 1 << 2;
// Finally, queue the event.
// NOTE: We are replacing the current device state wholesale here. An alternative
// would be to use QueueDeltaStateEvent to replace only select memory contents.
InputSystem.QueueStateEvent(this, state);
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c625211b3160b43799de4bd3f6ff4a54
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1 @@
This sample demonstrates how to add author a custom device that plugs into the input system.

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 2c783a24fcae4266ae1b3430c0dc4433
timeCreated: 1567712737

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 5ecbdcf62208440deb241b027d13487d
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,4 @@
{
"displayName": "Custom Device Usages",
"description": "Shows how to tag devices with custom usage strings that can be used, for example, to distinguish multiple instances of the same type of device (e.g. 'Gamepad') based on how the device is used (e.g. 'Player1' vs 'Player2' or 'LeftHand' vs 'RightHand')."
}

View File

@@ -0,0 +1,123 @@
using UnityEngine;
using UnityEngine.InputSystem;
#if UNITY_EDITOR
using UnityEditor;
#endif
// Say you want to distinguish a device not only by its type (e.g. "PS4 Controller")
// but also by the way it is used. This is a common scenario for VR controllers, for
// example, where the same type of controller may be used once in the left hand and
// once in the right hand. However, the need for distinguishing devices in a similar
// manner can pop up in a variety of situations. For example, on Switch it is used
// to distinguish the current orientation of the Joy-Con controller ("Horizontal" vs.
// "Vertical") allowing you to take orientation into account when binding actions.
//
// The input system allows you to distinguish devices based on the "usages" assigned
// to them. This is a generic mechanism that can be used to tag devices with arbitrary
// custom usages.
//
// To make this more concrete, let's say we have a game where two players control
// the game together each one using a gamepad but each receiving control over half
// the actions in the game.
//
// NOTE: What we do here is only one way to achieve this kind of setup. We could
// alternatively go and just create one control scheme for the first player
// and one control scheme for the second one and then have two PlayerInputs
// each using one of the two.
//
// So, what we'd like to do is tag one gamepad with "Player1" and one gamepad with
// with "Player2". Then, in the actions we can set up a binding scheme specifically
// for this style of play and bind actions such that are driven either from the
// first player's gamepad or from the second player's gamepad (or from either).
//
// The first bit we need for this is to tell the input system that "Player1" and
// "Player2" are usages that we intend to apply to gamepads. For this, we need
// to modify the "Gamepad" layout. We do so by applying what's called a "layout
// override". This needs to happen during initialization so here we go:
#if UNITY_EDITOR
[InitializeOnLoad]
#endif
public static class InitCustomDeviceUsages
{
static InitCustomDeviceUsages()
{
Initialize();
}
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
private static void Initialize()
{
// Here we register the layout override with the system.
//
// The layout override is just a fragment of layout information
// in JSON format.
//
// The key property here is "commonUsages" which tells the system
// that "Player1" and "Player2" are possible usages applied to devices
// using the given layout ("Gamepad" in our case).
InputSystem.RegisterLayoutOverride(@"
{
""name"" : ""GamepadPlayerUsageTags"",
""extend"" : ""Gamepad"",
""commonUsages"" : [
""Player1"", ""Player2""
]
}
");
// Now that we have done this, you will see that when using the
// control picker in the action editor, that there is now a
// "Gamepad (Player1)" and "Gamepad (Player2)" entry underneath
// "Gamepad". When you select those, you can bind specifically
// to a gamepad with the respective device usage.
//
// Also, you will now be able to *require* a device with the
// given usage in a control scheme. So, when creating a control
// scheme representing the shared Player1+Player2 controls,
// you can add one "Gamepad (Player1)" and one "Gamepad (Player2)"
// requirement.
//
// You can see an example setup for how this would look in an
// .inputactions file in the TwoPlayerControls.inputactions file
// that is part of this sample.
}
}
// However, we are still missing a piece. At runtime, no gamepad will
// receive either the "Player1" or the "Player2" usage assignment yet.
// So none of the bindings will work yet.
//
// To assign the usage tags to the devices, we need to call
// InputSystem.AddDeviceUsage or SetDeviceUsage.
//
// We could set this up any which way. As a demonstration, let's create
// a MonoBehaviour here that simply associates a specific tag with a
// specific gamepad index.
//
// In practice, you would probably want to do the assignment in a place
// where you handle your player setup/joining.
public class CustomDeviceUsages : MonoBehaviour
{
public int gamepadIndex;
public string usageTag;
private Gamepad m_Gamepad;
protected void OnEnable()
{
if (gamepadIndex >= 0 && gamepadIndex < Gamepad.all.Count)
{
m_Gamepad = Gamepad.all[gamepadIndex];
InputSystem.AddDeviceUsage(m_Gamepad, usageTag);
}
}
protected void OnDisable()
{
// If we grabbed a gamepad and it's still added to the system,
// remove the usage tag we added.
if (m_Gamepad != null && m_Gamepad.added)
InputSystem.RemoveDeviceUsage(m_Gamepad, usageTag);
m_Gamepad = null;
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 0f868c1ef5d55458da41f3690c690329
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,3 @@
This sample shows how to tag devices with custom "usages" and how to bind actions specifically to devices with only those usages.
This is useful if you have the same type of device that appears in multiple different roles that you want to distinguish when binding to the device. For example, when a device may appear in both the left and the right hand or may appear held in different orientations (say, horizontal vs vertical).

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: ef23f8f685c7b463e89b764da9062d6a
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,69 @@
{
"name": "TwoPlayerControls",
"maps": [
{
"name": "TwoPlayers",
"id": "0e22eb1c-8c6e-4cec-8364-2a9f0e3ef769",
"actions": [
{
"name": "Move",
"type": "Button",
"id": "d35725fa-073a-4e1c-9052-2c45b8ef0b4c",
"expectedControlType": "",
"processors": "",
"interactions": ""
},
{
"name": "Look",
"type": "Button",
"id": "249187b5-59c4-459f-84d0-731cb510c536",
"expectedControlType": "",
"processors": "",
"interactions": ""
}
],
"bindings": [
{
"name": "",
"id": "3e67c7ae-ca6c-4345-8f61-8df16fb87fec",
"path": "<Gamepad>{Player1}/leftStick",
"interactions": "",
"processors": "",
"groups": "TwoPlayers",
"action": "Move",
"isComposite": false,
"isPartOfComposite": false
},
{
"name": "",
"id": "46cadfba-140f-4895-8d9b-184da46b6010",
"path": "<Gamepad>{Player2}/rightStick",
"interactions": "",
"processors": "",
"groups": "TwoPlayers",
"action": "Look",
"isComposite": false,
"isPartOfComposite": false
}
]
}
],
"controlSchemes": [
{
"name": "TwoPlayers",
"bindingGroup": "TwoPlayers",
"devices": [
{
"devicePath": "<Gamepad>{Player1}",
"isOptional": false,
"isOR": false
},
{
"devicePath": "<Gamepad>{Player2}",
"isOptional": false,
"isOR": false
}
]
}
]
}

View File

@@ -0,0 +1,14 @@
fileFormatVersion: 2
guid: 917391a0a5f934196bf2608e2d195466
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 11500000, guid: 8404be70184654265930450def6a9037, type: 3}
generateWrapperCode: 0
wrapperCodePath:
wrapperClassName:
wrapperCodeNamespace:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 61dcbd3f75b1f4245adc2c94569f63c3
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,4 @@
{
"displayName": "Gamepad Mouse Cursor",
"description": "An example that shows how to use the gamepad for driving a mouse cursor for use with UIs."
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 237744bb87a5d144cb97babf7faefa00
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,71 @@
{
"name": "GamepadMouseCursorUIActions",
"maps": [
{
"name": "UI",
"id": "038d8f59-db9c-4389-83cd-7d41373518d5",
"actions": [
{
"name": "Point",
"type": "PassThrough",
"id": "81d2dc29-f5a6-47c1-a543-ad954fda9fa6",
"expectedControlType": "Vector2",
"processors": "",
"interactions": ""
},
{
"name": "Click",
"type": "PassThrough",
"id": "e5db52ef-8fc1-40c9-9799-57f91a7d7ade",
"expectedControlType": "Button",
"processors": "",
"interactions": ""
},
{
"name": "Scroll",
"type": "PassThrough",
"id": "48228e6f-8fda-4ba2-b461-fdae78c58090",
"expectedControlType": "Vector2",
"processors": "",
"interactions": ""
}
],
"bindings": [
{
"name": "",
"id": "39af39e2-c75c-421e-84bf-1e4ff9793ab4",
"path": "<VirtualMouse>/position",
"interactions": "",
"processors": "",
"groups": "",
"action": "Point",
"isComposite": false,
"isPartOfComposite": false
},
{
"name": "",
"id": "f9d5a98e-68a3-44c8-a096-a1a30237ca81",
"path": "<VirtualMouse>/leftButton",
"interactions": "",
"processors": "",
"groups": "",
"action": "Click",
"isComposite": false,
"isPartOfComposite": false
},
{
"name": "",
"id": "7ffd960c-11ef-4fc0-b91b-4cbd02eba1b8",
"path": "<VirtualMouse>/scroll",
"interactions": "",
"processors": "",
"groups": "",
"action": "Scroll",
"isComposite": false,
"isPartOfComposite": false
}
]
}
],
"controlSchemes": []
}

View File

@@ -0,0 +1,14 @@
fileFormatVersion: 2
guid: c2da68a33934478429331becc93eca32
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 11500000, guid: 8404be70184654265930450def6a9037, type: 3}
generateWrapperCode: 0
wrapperCodePath:
wrapperClassName:
wrapperCodeNamespace:

View File

@@ -0,0 +1,17 @@
Rather than adapting UIs for gamepad navigation/use, an oft-used alternative is to instead keep having UIs operated by pointer input but to drive the pointer from gamepad input.
This sample demonstrates how to set this up with the input system.
![Virtual Mouse Input Component](./VirtualMouseInput.png)
1) It uses a custom [actions file](./GamepadMouseCursorUIActions.inputactions) for feeding input to the UI as the default actions are set up for gamepad navigation &ndash; something we don't want here as it would conflict with gamepad input being used for virtual cursor navigation.
2) Note how `InputSystemUIInputModule` on the `EventSystem` GameObject is set up to reference actions from that file.
3) The key component to take a look at is `VirtualMouseInput` on `Canvas >> Cursor`. The component is set up to receive input from the gamepad and translates it into motion on the `RectTransform` it is given. When going into play mode, you should also see a `Virtual Mouse` being added to the devices by the component.
4) Note how the anchor position on the `RectTransform` is set to bottom left. This way the coordinate system responds to how mouse screen space operates.
5) Note how `Cursor` is the last child of `Canvas` so that it draws on top of everything else.
6) Note that `Raycast Target` on the `Image` component of the cursor is turned off to avoid raycasts from the mouse cursor hitting the cursor itself.
7) Note that `Cursor Mode` on the `VirtualMouseInput` component is set to `Hardware Cursor If Available`. This will cause the component to look for a system mouse. If present, the system mouse is disabled and the system mouse cursor is warped to the virtual mouse position using `Mouse.WarpCursorPosition`. If no system mouse is present, `Cursor Graphic` will be used as a software mouse cursor.
# Licenses
The [cursor](./crosshair.png) used in the example is from [game-icons.net](https://game-icons.net/1x1/delapouite/crosshair.html) and made by [Delapuite](https://delapouite.com/) and released under the [CC BY 3.0 license](https://creativecommons.org/licenses/by/3.0/). It is used without modifications.

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 12394b570bf2e894890bf79b26b84620
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

View File

@@ -0,0 +1,91 @@
fileFormatVersion: 2
guid: 137530feef62723499ce9521b344bc38
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 10
mipmaps:
mipMapMode: 0
enableMipMap: 1
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: -1
aniso: -1
mipBias: -100
wrapU: -1
wrapV: -1
wrapW: -1
nPOTScale: 1
lightmap: 0
compressionQuality: 50
spriteMode: 0
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 0
spriteTessellationDetail: -1
textureType: 0
textureShape: 1
singleChannelComponent: 0
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
platformSettings:
- serializedVersion: 3
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
physicsShape: []
bones: []
spriteID:
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spritePackingTag:
pSDRemoveMatte: 0
pSDShowRemoveMatteOption: 0
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 KiB

View File

@@ -0,0 +1,103 @@
fileFormatVersion: 2
guid: c08500e75c1581d4daacd1a363ff46f6
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 10
mipmaps:
mipMapMode: 0
enableMipMap: 0
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: -1
aniso: -1
mipBias: -100
wrapU: 1
wrapV: 1
wrapW: -1
nPOTScale: 0
lightmap: 0
compressionQuality: 50
spriteMode: 1
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 1
spriteTessellationDetail: -1
textureType: 8
textureShape: 1
singleChannelComponent: 0
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
platformSettings:
- serializedVersion: 3
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 1
- serializedVersion: 3
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 1
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
physicsShape: []
bones: []
spriteID: 5e97eb03825dee720800000000000000
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spritePackingTag:
pSDRemoveMatte: 0
pSDShowRemoveMatteOption: 0
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 7bb007822c994c64ab31f2a271cdfc23
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,136 @@
using UnityEngine.InputSystem.LowLevel;
using UnityEngine.UI;
namespace UnityEngine.InputSystem.Samples.IMEInput
{
/// <summary>
/// An example IME Input Handler showing how IME input can be handled using
/// input system provided events.
/// </summary>
/// <seealso cref="Keyboard.OnIMECompositionChanged"/>
public class IMEInputHandler : MonoBehaviour
{
// Handles text passed via Keyboard.onTextInput event
private void OnTextInput(char character)
{
AssertReferencesAreValid();
// Assumes the current IME composition text has been submitted
if (m_ComposingViaIME)
{
m_CompositionField.text = string.Empty;
m_ComposingViaIME = false;
}
m_CurrentInput += character;
m_TextField.text = m_CurrentInput;
m_CombinedField.text = m_CurrentInput;
}
// Handles text passed via Keyboard.onIMECompositionChange event
private void OnIMECompositionChange(IMECompositionString text)
{
AssertReferencesAreValid();
// IME composition strings without length can also mean
// the composition has been submitted
if (text.Count == 0)
{
m_ComposingViaIME = false;
m_CompositionField.text = string.Empty;
return;
}
var compositionText = text.ToString();
m_ComposingViaIME = true;
m_CompositionField.text = compositionText;
// The combined text contains both the current input and the current status of the composition
m_CombinedField.text = string.Format("{0}{1}", m_CurrentInput, compositionText);
}
// Adds keyboard and input field listeners
private void OnEnable()
{
if (m_EventListenersAdded)
return;
var keyboard = InputSystem.GetDevice<Keyboard>();
if (keyboard is null)
return;
keyboard.onTextInput += OnTextInput;
keyboard.onIMECompositionChange += OnIMECompositionChange;
m_InputField.onValueChanged.AddListener(OnValueChanged);
m_EventListenersAdded = true;
}
// Removes keyboard and input field listeners
private void OnDisable()
{
if (!m_EventListenersAdded)
return;
var keyboard = InputSystem.GetDevice<Keyboard>();
if (keyboard is null)
return;
keyboard.onTextInput -= OnTextInput;
keyboard.onIMECompositionChange -= OnIMECompositionChange;
m_InputField.onValueChanged.RemoveListener(OnValueChanged);
Clear();
m_EventListenersAdded = false;
}
// Called when the input field's text is changed
private void OnValueChanged(string value)
{
AssertReferencesAreValid();
if (!string.IsNullOrEmpty(value))
return;
Clear();
}
// Clears the text from all of the fields
private void Clear()
{
m_CompositionField.text = string.Empty;
m_TextField.text = string.Empty;
m_CombinedField.text = string.Empty;
m_CurrentInput = string.Empty;
m_ComposingViaIME = false;
}
// Ensures all fields are correctly referenced
private void AssertReferencesAreValid()
{
Debug.Assert(m_CompositionField != null, "Composition field cannot be null");
Debug.Assert(m_TextField != null, "Text field field cannot be null");
Debug.Assert(m_CombinedField != null, "Combined field field cannot be null");
Debug.Assert(m_InputField != null, "Input field field cannot be null");
}
[Tooltip("Text field intended to display the string received via OnIMECompositionChanged")]
[SerializeField] private InputField m_CompositionField;
[Tooltip("Text field intended to display characters received via OnTextInput")]
[SerializeField] private InputField m_TextField;
[Tooltip("Text field intended to display a combination of characters received by "
+ "both the OnIMECompositionChanged & OnTextInput events")]
[SerializeField] private InputField m_CombinedField;
[Tooltip("Text field intended for user input")]
[SerializeField] private InputField m_InputField;
private bool m_EventListenersAdded = false;
private bool m_ComposingViaIME = false;
private string m_CurrentInput;
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1d6911df4ad59ac4fa65f35ceabfeb06
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 82c13dd0eb11e0d44adf707fdc51a350
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: a91a0dc3ff86dc64890b3eafb9713d46
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,4 @@
{
"displayName": "In-Game Hints",
"description": "Demonstrates how to create in-game hints in the UI which reflect current bindings and active control schemes."
}

View File

@@ -0,0 +1,440 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was auto-generated by com.unity.inputsystem:InputActionCodeGenerator
// version 1.11.2
// from Assets/Samples/InGameHints/InGameHintsActions.inputactions
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine.InputSystem;
using UnityEngine.InputSystem.Utilities;
namespace UnityEngine.InputSystem.Samples.InGameHints
{
public partial class @InGameHintsActions: IInputActionCollection2, IDisposable
{
public InputActionAsset asset { get; }
public @InGameHintsActions()
{
asset = InputActionAsset.FromJson(@"{
""name"": ""InGameHintsActions"",
""maps"": [
{
""name"": ""Gameplay"",
""id"": ""9af2d1b0-cc47-4300-854c-838acb4b168b"",
""actions"": [
{
""name"": ""Move"",
""type"": ""Value"",
""id"": ""7e7492e7-1329-48bb-9fdc-279fd15473b4"",
""expectedControlType"": ""Vector2"",
""processors"": """",
""interactions"": """",
""initialStateCheck"": true
},
{
""name"": ""Look"",
""type"": ""Value"",
""id"": ""981fecc2-2e7a-4d6a-b041-00b47626e0a1"",
""expectedControlType"": ""Vector2"",
""processors"": """",
""interactions"": """",
""initialStateCheck"": true
},
{
""name"": ""PickUp"",
""type"": ""Button"",
""id"": ""5a59bbc2-a3d4-4cbd-88bb-01120d97dc69"",
""expectedControlType"": """",
""processors"": """",
""interactions"": """",
""initialStateCheck"": false
},
{
""name"": ""Drop"",
""type"": ""Button"",
""id"": ""f37bbe7e-e241-443f-b868-c784e1219f25"",
""expectedControlType"": """",
""processors"": """",
""interactions"": """",
""initialStateCheck"": false
},
{
""name"": ""Throw"",
""type"": ""Button"",
""id"": ""e450d71c-7cc5-4879-afb5-f3ed682d9824"",
""expectedControlType"": """",
""processors"": """",
""interactions"": """",
""initialStateCheck"": false
}
],
""bindings"": [
{
""name"": """",
""id"": ""5abc4d20-74bd-4f14-902f-2bd2cf59cc28"",
""path"": ""<Gamepad>/leftStick"",
""interactions"": """",
""processors"": """",
""groups"": ""Gamepad"",
""action"": ""Move"",
""isComposite"": false,
""isPartOfComposite"": false
},
{
""name"": ""WASD"",
""id"": ""b16141b1-1611-44db-9576-5a004eb451f2"",
""path"": ""2DVector"",
""interactions"": """",
""processors"": """",
""groups"": ""Keyboard&Mouse"",
""action"": ""Move"",
""isComposite"": true,
""isPartOfComposite"": false
},
{
""name"": ""up"",
""id"": ""2b20de3f-1ad8-4b42-b591-595edf60dced"",
""path"": ""<Keyboard>/w"",
""interactions"": """",
""processors"": """",
""groups"": ""Keyboard&Mouse"",
""action"": ""Move"",
""isComposite"": false,
""isPartOfComposite"": true
},
{
""name"": ""down"",
""id"": ""2f92eaa9-7f1f-4f42-9682-d105f7c2fc22"",
""path"": ""<Keyboard>/s"",
""interactions"": """",
""processors"": """",
""groups"": ""Keyboard&Mouse"",
""action"": ""Move"",
""isComposite"": false,
""isPartOfComposite"": true
},
{
""name"": ""left"",
""id"": ""3ba79a56-c5f8-4999-8203-bef8471f4bd8"",
""path"": ""<Keyboard>/a"",
""interactions"": """",
""processors"": """",
""groups"": ""Keyboard&Mouse"",
""action"": ""Move"",
""isComposite"": false,
""isPartOfComposite"": true
},
{
""name"": ""right"",
""id"": ""8d9acfe6-d844-4860-a151-01d6eb0dfb48"",
""path"": ""<Keyboard>/d"",
""interactions"": """",
""processors"": """",
""groups"": ""Keyboard&Mouse"",
""action"": ""Move"",
""isComposite"": false,
""isPartOfComposite"": true
},
{
""name"": """",
""id"": ""b69cbeb7-a5bf-4df1-8965-17d944634cef"",
""path"": ""<Gamepad>/rightStick"",
""interactions"": """",
""processors"": """",
""groups"": ""Gamepad"",
""action"": ""Look"",
""isComposite"": false,
""isPartOfComposite"": false
},
{
""name"": """",
""id"": ""b2ddefc9-49da-485d-be28-58e3ec3f8080"",
""path"": ""<Mouse>/delta"",
""interactions"": """",
""processors"": """",
""groups"": ""Keyboard&Mouse"",
""action"": ""Look"",
""isComposite"": false,
""isPartOfComposite"": false
},
{
""name"": """",
""id"": ""92182492-7b62-47e0-94ad-53d9937d9905"",
""path"": ""<Gamepad>/buttonSouth"",
""interactions"": """",
""processors"": """",
""groups"": ""Gamepad"",
""action"": ""PickUp"",
""isComposite"": false,
""isPartOfComposite"": false
},
{
""name"": """",
""id"": ""e20635aa-ffe7-4ed9-8802-96c039d26a8f"",
""path"": ""<Keyboard>/q"",
""interactions"": """",
""processors"": """",
""groups"": ""Keyboard&Mouse"",
""action"": ""PickUp"",
""isComposite"": false,
""isPartOfComposite"": false
},
{
""name"": """",
""id"": ""f5571cd9-1166-4ddc-9071-37dc597b1d4e"",
""path"": ""<Gamepad>/buttonEast"",
""interactions"": """",
""processors"": """",
""groups"": ""Gamepad"",
""action"": ""Drop"",
""isComposite"": false,
""isPartOfComposite"": false
},
{
""name"": """",
""id"": ""950f549e-ec9c-4d03-aeff-f09ec4031d01"",
""path"": ""<Keyboard>/e"",
""interactions"": """",
""processors"": """",
""groups"": ""Keyboard&Mouse"",
""action"": ""Drop"",
""isComposite"": false,
""isPartOfComposite"": false
},
{
""name"": """",
""id"": ""47d1952d-797b-4f5b-986c-654b8e479deb"",
""path"": ""<Gamepad>/buttonSouth"",
""interactions"": """",
""processors"": """",
""groups"": ""Gamepad"",
""action"": ""Throw"",
""isComposite"": false,
""isPartOfComposite"": false
},
{
""name"": """",
""id"": ""02e2493d-1eb3-4334-9d25-92f2b5e21399"",
""path"": ""<Keyboard>/space"",
""interactions"": """",
""processors"": """",
""groups"": ""Keyboard&Mouse"",
""action"": ""Throw"",
""isComposite"": false,
""isPartOfComposite"": false
}
]
}
],
""controlSchemes"": [
{
""name"": ""Gamepad"",
""bindingGroup"": ""Gamepad"",
""devices"": [
{
""devicePath"": ""<Gamepad>"",
""isOptional"": false,
""isOR"": false
}
]
},
{
""name"": ""Keyboard&Mouse"",
""bindingGroup"": ""Keyboard&Mouse"",
""devices"": [
{
""devicePath"": ""<Keyboard>"",
""isOptional"": false,
""isOR"": false
},
{
""devicePath"": ""<Mouse>"",
""isOptional"": false,
""isOR"": false
}
]
}
]
}");
// Gameplay
m_Gameplay = asset.FindActionMap("Gameplay", throwIfNotFound: true);
m_Gameplay_Move = m_Gameplay.FindAction("Move", throwIfNotFound: true);
m_Gameplay_Look = m_Gameplay.FindAction("Look", throwIfNotFound: true);
m_Gameplay_PickUp = m_Gameplay.FindAction("PickUp", throwIfNotFound: true);
m_Gameplay_Drop = m_Gameplay.FindAction("Drop", throwIfNotFound: true);
m_Gameplay_Throw = m_Gameplay.FindAction("Throw", throwIfNotFound: true);
}
~@InGameHintsActions()
{
UnityEngine.Debug.Assert(!m_Gameplay.enabled, "This will cause a leak and performance issues, InGameHintsActions.Gameplay.Disable() has not been called.");
}
public void Dispose()
{
UnityEngine.Object.Destroy(asset);
}
public InputBinding? bindingMask
{
get => asset.bindingMask;
set => asset.bindingMask = value;
}
public ReadOnlyArray<InputDevice>? devices
{
get => asset.devices;
set => asset.devices = value;
}
public ReadOnlyArray<InputControlScheme> controlSchemes => asset.controlSchemes;
public bool Contains(InputAction action)
{
return asset.Contains(action);
}
public IEnumerator<InputAction> GetEnumerator()
{
return asset.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
public void Enable()
{
asset.Enable();
}
public void Disable()
{
asset.Disable();
}
public IEnumerable<InputBinding> bindings => asset.bindings;
public InputAction FindAction(string actionNameOrId, bool throwIfNotFound = false)
{
return asset.FindAction(actionNameOrId, throwIfNotFound);
}
public int FindBinding(InputBinding bindingMask, out InputAction action)
{
return asset.FindBinding(bindingMask, out action);
}
// Gameplay
private readonly InputActionMap m_Gameplay;
private List<IGameplayActions> m_GameplayActionsCallbackInterfaces = new List<IGameplayActions>();
private readonly InputAction m_Gameplay_Move;
private readonly InputAction m_Gameplay_Look;
private readonly InputAction m_Gameplay_PickUp;
private readonly InputAction m_Gameplay_Drop;
private readonly InputAction m_Gameplay_Throw;
public struct GameplayActions
{
private @InGameHintsActions m_Wrapper;
public GameplayActions(@InGameHintsActions wrapper) { m_Wrapper = wrapper; }
public InputAction @Move => m_Wrapper.m_Gameplay_Move;
public InputAction @Look => m_Wrapper.m_Gameplay_Look;
public InputAction @PickUp => m_Wrapper.m_Gameplay_PickUp;
public InputAction @Drop => m_Wrapper.m_Gameplay_Drop;
public InputAction @Throw => m_Wrapper.m_Gameplay_Throw;
public InputActionMap Get() { return m_Wrapper.m_Gameplay; }
public void Enable() { Get().Enable(); }
public void Disable() { Get().Disable(); }
public bool enabled => Get().enabled;
public static implicit operator InputActionMap(GameplayActions set) { return set.Get(); }
public void AddCallbacks(IGameplayActions instance)
{
if (instance == null || m_Wrapper.m_GameplayActionsCallbackInterfaces.Contains(instance)) return;
m_Wrapper.m_GameplayActionsCallbackInterfaces.Add(instance);
@Move.started += instance.OnMove;
@Move.performed += instance.OnMove;
@Move.canceled += instance.OnMove;
@Look.started += instance.OnLook;
@Look.performed += instance.OnLook;
@Look.canceled += instance.OnLook;
@PickUp.started += instance.OnPickUp;
@PickUp.performed += instance.OnPickUp;
@PickUp.canceled += instance.OnPickUp;
@Drop.started += instance.OnDrop;
@Drop.performed += instance.OnDrop;
@Drop.canceled += instance.OnDrop;
@Throw.started += instance.OnThrow;
@Throw.performed += instance.OnThrow;
@Throw.canceled += instance.OnThrow;
}
private void UnregisterCallbacks(IGameplayActions instance)
{
@Move.started -= instance.OnMove;
@Move.performed -= instance.OnMove;
@Move.canceled -= instance.OnMove;
@Look.started -= instance.OnLook;
@Look.performed -= instance.OnLook;
@Look.canceled -= instance.OnLook;
@PickUp.started -= instance.OnPickUp;
@PickUp.performed -= instance.OnPickUp;
@PickUp.canceled -= instance.OnPickUp;
@Drop.started -= instance.OnDrop;
@Drop.performed -= instance.OnDrop;
@Drop.canceled -= instance.OnDrop;
@Throw.started -= instance.OnThrow;
@Throw.performed -= instance.OnThrow;
@Throw.canceled -= instance.OnThrow;
}
public void RemoveCallbacks(IGameplayActions instance)
{
if (m_Wrapper.m_GameplayActionsCallbackInterfaces.Remove(instance))
UnregisterCallbacks(instance);
}
public void SetCallbacks(IGameplayActions instance)
{
foreach (var item in m_Wrapper.m_GameplayActionsCallbackInterfaces)
UnregisterCallbacks(item);
m_Wrapper.m_GameplayActionsCallbackInterfaces.Clear();
AddCallbacks(instance);
}
}
public GameplayActions @Gameplay => new GameplayActions(this);
private int m_GamepadSchemeIndex = -1;
public InputControlScheme GamepadScheme
{
get
{
if (m_GamepadSchemeIndex == -1) m_GamepadSchemeIndex = asset.FindControlSchemeIndex("Gamepad");
return asset.controlSchemes[m_GamepadSchemeIndex];
}
}
private int m_KeyboardMouseSchemeIndex = -1;
public InputControlScheme KeyboardMouseScheme
{
get
{
if (m_KeyboardMouseSchemeIndex == -1) m_KeyboardMouseSchemeIndex = asset.FindControlSchemeIndex("Keyboard&Mouse");
return asset.controlSchemes[m_KeyboardMouseSchemeIndex];
}
}
public interface IGameplayActions
{
void OnMove(InputAction.CallbackContext context);
void OnLook(InputAction.CallbackContext context);
void OnPickUp(InputAction.CallbackContext context);
void OnDrop(InputAction.CallbackContext context);
void OnThrow(InputAction.CallbackContext context);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 30718f5974310d148a77ab1e420b1b83
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,241 @@
{
"name": "InGameHintsActions",
"maps": [
{
"name": "Gameplay",
"id": "9af2d1b0-cc47-4300-854c-838acb4b168b",
"actions": [
{
"name": "Move",
"type": "Value",
"id": "7e7492e7-1329-48bb-9fdc-279fd15473b4",
"expectedControlType": "Vector2",
"processors": "",
"interactions": "",
"initialStateCheck": true
},
{
"name": "Look",
"type": "Value",
"id": "981fecc2-2e7a-4d6a-b041-00b47626e0a1",
"expectedControlType": "Vector2",
"processors": "",
"interactions": "",
"initialStateCheck": true
},
{
"name": "PickUp",
"type": "Button",
"id": "5a59bbc2-a3d4-4cbd-88bb-01120d97dc69",
"expectedControlType": "",
"processors": "",
"interactions": "",
"initialStateCheck": false
},
{
"name": "Drop",
"type": "Button",
"id": "f37bbe7e-e241-443f-b868-c784e1219f25",
"expectedControlType": "",
"processors": "",
"interactions": "",
"initialStateCheck": false
},
{
"name": "Throw",
"type": "Button",
"id": "e450d71c-7cc5-4879-afb5-f3ed682d9824",
"expectedControlType": "",
"processors": "",
"interactions": "",
"initialStateCheck": false
}
],
"bindings": [
{
"name": "",
"id": "5abc4d20-74bd-4f14-902f-2bd2cf59cc28",
"path": "<Gamepad>/leftStick",
"interactions": "",
"processors": "",
"groups": "Gamepad",
"action": "Move",
"isComposite": false,
"isPartOfComposite": false
},
{
"name": "WASD",
"id": "b16141b1-1611-44db-9576-5a004eb451f2",
"path": "2DVector",
"interactions": "",
"processors": "",
"groups": "Keyboard&Mouse",
"action": "Move",
"isComposite": true,
"isPartOfComposite": false
},
{
"name": "up",
"id": "2b20de3f-1ad8-4b42-b591-595edf60dced",
"path": "<Keyboard>/w",
"interactions": "",
"processors": "",
"groups": "Keyboard&Mouse",
"action": "Move",
"isComposite": false,
"isPartOfComposite": true
},
{
"name": "down",
"id": "2f92eaa9-7f1f-4f42-9682-d105f7c2fc22",
"path": "<Keyboard>/s",
"interactions": "",
"processors": "",
"groups": "Keyboard&Mouse",
"action": "Move",
"isComposite": false,
"isPartOfComposite": true
},
{
"name": "left",
"id": "3ba79a56-c5f8-4999-8203-bef8471f4bd8",
"path": "<Keyboard>/a",
"interactions": "",
"processors": "",
"groups": "Keyboard&Mouse",
"action": "Move",
"isComposite": false,
"isPartOfComposite": true
},
{
"name": "right",
"id": "8d9acfe6-d844-4860-a151-01d6eb0dfb48",
"path": "<Keyboard>/d",
"interactions": "",
"processors": "",
"groups": "Keyboard&Mouse",
"action": "Move",
"isComposite": false,
"isPartOfComposite": true
},
{
"name": "",
"id": "b69cbeb7-a5bf-4df1-8965-17d944634cef",
"path": "<Gamepad>/rightStick",
"interactions": "",
"processors": "",
"groups": "Gamepad",
"action": "Look",
"isComposite": false,
"isPartOfComposite": false
},
{
"name": "",
"id": "b2ddefc9-49da-485d-be28-58e3ec3f8080",
"path": "<Mouse>/delta",
"interactions": "",
"processors": "",
"groups": "Keyboard&Mouse",
"action": "Look",
"isComposite": false,
"isPartOfComposite": false
},
{
"name": "",
"id": "92182492-7b62-47e0-94ad-53d9937d9905",
"path": "<Gamepad>/buttonSouth",
"interactions": "",
"processors": "",
"groups": "Gamepad",
"action": "PickUp",
"isComposite": false,
"isPartOfComposite": false
},
{
"name": "",
"id": "e20635aa-ffe7-4ed9-8802-96c039d26a8f",
"path": "<Keyboard>/q",
"interactions": "",
"processors": "",
"groups": "Keyboard&Mouse",
"action": "PickUp",
"isComposite": false,
"isPartOfComposite": false
},
{
"name": "",
"id": "f5571cd9-1166-4ddc-9071-37dc597b1d4e",
"path": "<Gamepad>/buttonEast",
"interactions": "",
"processors": "",
"groups": "Gamepad",
"action": "Drop",
"isComposite": false,
"isPartOfComposite": false
},
{
"name": "",
"id": "950f549e-ec9c-4d03-aeff-f09ec4031d01",
"path": "<Keyboard>/e",
"interactions": "",
"processors": "",
"groups": "Keyboard&Mouse",
"action": "Drop",
"isComposite": false,
"isPartOfComposite": false
},
{
"name": "",
"id": "47d1952d-797b-4f5b-986c-654b8e479deb",
"path": "<Gamepad>/buttonSouth",
"interactions": "",
"processors": "",
"groups": "Gamepad",
"action": "Throw",
"isComposite": false,
"isPartOfComposite": false
},
{
"name": "",
"id": "02e2493d-1eb3-4334-9d25-92f2b5e21399",
"path": "<Keyboard>/space",
"interactions": "",
"processors": "",
"groups": "Keyboard&Mouse",
"action": "Throw",
"isComposite": false,
"isPartOfComposite": false
}
]
}
],
"controlSchemes": [
{
"name": "Gamepad",
"bindingGroup": "Gamepad",
"devices": [
{
"devicePath": "<Gamepad>",
"isOptional": false,
"isOR": false
}
]
},
{
"name": "Keyboard&Mouse",
"bindingGroup": "Keyboard&Mouse",
"devices": [
{
"devicePath": "<Keyboard>",
"isOptional": false,
"isOR": false
},
{
"devicePath": "<Mouse>",
"isOptional": false,
"isOR": false
}
]
}
]
}

View File

@@ -0,0 +1,14 @@
fileFormatVersion: 2
guid: 6bb4ca8e0c27ed041910c04f94cf6c7e
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 11500000, guid: 8404be70184654265930450def6a9037, type: 3}
generateWrapperCode: 1
wrapperCodePath:
wrapperClassName:
wrapperCodeNamespace: UnityEngine.InputSystem.Samples.InGameHints

View File

@@ -0,0 +1,235 @@
// This example demonstrates how to display text in the UI that involves action bindings.
// When the player switches control schemes or customizes controls (the latter is not set up
// in this example but if supported, would work with the existing code as is), text that
// is shown to the user may be affected.
//
// In the example, the player is able to move around the world and look at objects (simple
// cubes). When an object is in sight, the player can pick the object with a button. While
// having an object picked up, the player can then either throw the object or drop it back
// on the ground.
//
// Depending on the current context, we display hints in the UI that reflect the currently
// active bindings.
using UnityEngine.UI;
namespace UnityEngine.InputSystem.Samples.InGameHints
{
public class InGameHintsExample : MonoBehaviour
{
public Text helpText;
public float moveSpeed;
public float rotateSpeed;
public float throwForce;
public float pickupDistance;
public float holdDistance;
private Vector2 m_Rotation;
private enum State
{
Wandering,
ObjectInSights,
ObjectPickedUp
}
private PlayerInput m_PlayerInput;
private State m_CurrentState;
private Transform m_CurrentObject;
private MaterialPropertyBlock m_PropertyBlock;
// Cached help texts so that we don't generate garbage all the time. Could even cache them by control
// scheme to not create garbage during control scheme switching but we consider control scheme switches
// rare so not worth the extra cost in complexity and memory.
private string m_LookAtObjectHelpText;
private string m_ThrowObjectHelpText;
private const string kDefaultHelpTextFormat = "Move close to one of the cubes and look at it to pick up";
private const string kLookAtObjectHelpTextFormat = "Press {pickup} to pick object up";
private const string kThrowObjectHelpTextFormat = "Press {throw} to throw object; press {drop} to drop object";
public void Awake()
{
m_PlayerInput = GetComponent<PlayerInput>();
}
public void OnEnable()
{
ChangeState(State.Wandering);
}
// This is invoked by PlayerInput when the controls on the player change. If the player switches control
// schemes or keyboard layouts, we end up here and re-generate our hints.
public void OnControlsChanged()
{
UpdateUIHints(regenerate: true); // Force re-generation of our cached text strings to pick up new bindings.
}
private int m_UpdateCount;
public void Update()
{
var move = m_PlayerInput.actions["move"].ReadValue<Vector2>();
var look = m_PlayerInput.actions["look"].ReadValue<Vector2>();
Move(move);
Look(look);
switch (m_CurrentState)
{
case State.Wandering:
case State.ObjectInSights:
// While looking around for an object to pick up, we constantly raycast into the world.
if (Physics.Raycast(transform.position, transform.forward, out var hitInfo,
pickupDistance) && !hitInfo.collider.gameObject.isStatic)
{
if (m_CurrentState != State.ObjectInSights)
ChangeState(State.ObjectInSights);
m_CurrentObject = hitInfo.transform;
// Set a custom color override on the object by installing our property block.
if (m_PropertyBlock == null)
{
m_PropertyBlock = new MaterialPropertyBlock();
m_PropertyBlock.SetColor("_Color", new Color(0.75f, 0, 0));
}
m_CurrentObject.GetComponent<MeshRenderer>().SetPropertyBlock(m_PropertyBlock);
}
else if (m_CurrentState != State.Wandering)
{
// No longer have object in sight.
ChangeState(State.Wandering);
if (m_CurrentObject != null)
{
// Clear property block on renderer to get rid of our custom color override.
m_CurrentObject.GetComponent<Renderer>().SetPropertyBlock(null);
m_CurrentObject = null;
}
}
if (m_PlayerInput.actions["pickup"].triggered && m_CurrentObject != null)
{
PickUp();
ChangeState(State.ObjectPickedUp);
}
break;
case State.ObjectPickedUp:
// If the player hits the throw button, throw the currently carried object.
// For this example, let's call this good enough. In a real game, we'd want to avoid the raycast
if (m_PlayerInput.actions["throw"].triggered)
{
Throw();
ChangeState(State.Wandering);
}
else if (m_PlayerInput.actions["drop"].triggered)
{
Throw(drop: true);
ChangeState(State.Wandering);
}
break;
}
}
private void ChangeState(State newState)
{
switch (newState)
{
case State.Wandering:
break;
case State.ObjectInSights:
break;
case State.ObjectPickedUp:
break;
}
m_CurrentState = newState;
UpdateUIHints();
}
private void UpdateUIHints(bool regenerate = false)
{
if (regenerate)
{
m_ThrowObjectHelpText = default;
m_LookAtObjectHelpText = default;
}
switch (m_CurrentState)
{
case State.ObjectInSights:
if (m_LookAtObjectHelpText == null)
m_LookAtObjectHelpText = kLookAtObjectHelpTextFormat.Replace("{pickup}",
m_PlayerInput.actions["pickup"].GetBindingDisplayString());
helpText.text = m_LookAtObjectHelpText;
break;
case State.ObjectPickedUp:
if (m_ThrowObjectHelpText == null)
m_ThrowObjectHelpText = kThrowObjectHelpTextFormat
.Replace("{throw}", m_PlayerInput.actions["throw"].GetBindingDisplayString())
.Replace("{drop}", m_PlayerInput.actions["drop"].GetBindingDisplayString());
helpText.text = m_ThrowObjectHelpText;
break;
default:
helpText.text = kDefaultHelpTextFormat;
break;
}
}
// Throw or drop currently picked up object.
private void Throw(bool drop = false)
{
// Unmount it.
m_CurrentObject.parent = null;
// Turn physics back on.
var rigidBody = m_CurrentObject.GetComponent<Rigidbody>();
rigidBody.isKinematic = false;
// Apply force.
if (!drop)
rigidBody.AddForce(transform.forward * throwForce, ForceMode.Impulse);
m_CurrentObject = null;
}
private void PickUp()
{
// Mount to our transform.
m_CurrentObject.position = default;
m_CurrentObject.SetParent(transform, worldPositionStays: false);
m_CurrentObject.localPosition += new Vector3(0, 0, holdDistance);
// Remove color override.
m_CurrentObject.GetComponent<Renderer>().SetPropertyBlock(null);
// We don't want the object to be governed by physics while we hold it so turn it into a
// kinematics body.
m_CurrentObject.GetComponent<Rigidbody>().isKinematic = true;
}
private void Move(Vector2 direction)
{
if (direction.sqrMagnitude < 0.01)
return;
var scaledMoveSpeed = moveSpeed * Time.deltaTime;
// For simplicity's sake, we just keep movement in a single plane here. Rotate
// direction according to world Y rotation of player.
var move = Quaternion.Euler(0, transform.eulerAngles.y, 0) * new Vector3(direction.x, 0, direction.y);
transform.position += move * scaledMoveSpeed;
}
private void Look(Vector2 rotate)
{
if (rotate.sqrMagnitude < 0.01)
return;
var scaledRotateSpeed = rotateSpeed * Time.deltaTime;
m_Rotation.y += rotate.x * scaledRotateSpeed;
m_Rotation.x = Mathf.Clamp(m_Rotation.x - rotate.y * scaledRotateSpeed, -89, 89);
transform.localEulerAngles = m_Rotation;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f7157678e0953b24eb3ce108d2e34e37
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: be0c3658cdfdbf84f8cd833d30749454
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,16 @@
{
"name": "Unity.InputSystem.InGameHints",
"references": [
"GUID:75469ad4d38634e559750d17036d5f7c",
"GUID:2bafac87e7f4b9b418d9448d219b01ab"
],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 043547d79c83ab74e9bcfef914059964
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: a4bee43ed77c3411b8020daede403ff4
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

Some files were not shown because too many files have changed in this diff Show More