UI 자동화를 위해 바인딩 기능 구현

- 유니티 에셋 인증 오류로 meta 재생성
This commit is contained in:
2026-01-25 01:31:34 +09:00
parent 2ceb28f55d
commit ce83f21c93
1861 changed files with 377882 additions and 211 deletions

View File

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

View File

@@ -0,0 +1,839 @@
{
"version": 1,
"name": "DefaultInputActions",
"maps": [
{
"name": "Player",
"id": "df70fa95-8a34-4494-b137-73ab6b9c7d37",
"actions": [
{
"name": "Move",
"type": "Value",
"id": "351f2ccd-1f9f-44bf-9bec-d62ac5c5f408",
"expectedControlType": "Vector2",
"processors": "",
"interactions": "",
"initialStateCheck": true
},
{
"name": "Look",
"type": "Value",
"id": "6b444451-8a00-4d00-a97e-f47457f736a8",
"expectedControlType": "Vector2",
"processors": "",
"interactions": "",
"initialStateCheck": true
},
{
"name": "Fire",
"type": "Button",
"id": "6c2ab1b8-8984-453a-af3d-a3c78ae1679a",
"expectedControlType": "Button",
"processors": "",
"interactions": "",
"initialStateCheck": false
}
],
"bindings": [
{
"name": "",
"id": "978bfe49-cc26-4a3d-ab7b-7d7a29327403",
"path": "<Gamepad>/leftStick",
"interactions": "",
"processors": "",
"groups": ";Gamepad",
"action": "Move",
"isComposite": false,
"isPartOfComposite": false
},
{
"name": "WASD",
"id": "00ca640b-d935-4593-8157-c05846ea39b3",
"path": "Dpad",
"interactions": "",
"processors": "",
"groups": "",
"action": "Move",
"isComposite": true,
"isPartOfComposite": false
},
{
"name": "up",
"id": "e2062cb9-1b15-46a2-838c-2f8d72a0bdd9",
"path": "<Keyboard>/w",
"interactions": "",
"processors": "",
"groups": ";Keyboard&Mouse",
"action": "Move",
"isComposite": false,
"isPartOfComposite": true
},
{
"name": "up",
"id": "8180e8bd-4097-4f4e-ab88-4523101a6ce9",
"path": "<Keyboard>/upArrow",
"interactions": "",
"processors": "",
"groups": ";Keyboard&Mouse",
"action": "Move",
"isComposite": false,
"isPartOfComposite": true
},
{
"name": "down",
"id": "320bffee-a40b-4347-ac70-c210eb8bc73a",
"path": "<Keyboard>/s",
"interactions": "",
"processors": "",
"groups": ";Keyboard&Mouse",
"action": "Move",
"isComposite": false,
"isPartOfComposite": true
},
{
"name": "down",
"id": "1c5327b5-f71c-4f60-99c7-4e737386f1d1",
"path": "<Keyboard>/downArrow",
"interactions": "",
"processors": "",
"groups": ";Keyboard&Mouse",
"action": "Move",
"isComposite": false,
"isPartOfComposite": true
},
{
"name": "left",
"id": "d2581a9b-1d11-4566-b27d-b92aff5fabbc",
"path": "<Keyboard>/a",
"interactions": "",
"processors": "",
"groups": ";Keyboard&Mouse",
"action": "Move",
"isComposite": false,
"isPartOfComposite": true
},
{
"name": "left",
"id": "2e46982e-44cc-431b-9f0b-c11910bf467a",
"path": "<Keyboard>/leftArrow",
"interactions": "",
"processors": "",
"groups": ";Keyboard&Mouse",
"action": "Move",
"isComposite": false,
"isPartOfComposite": true
},
{
"name": "right",
"id": "fcfe95b8-67b9-4526-84b5-5d0bc98d6400",
"path": "<Keyboard>/d",
"interactions": "",
"processors": "",
"groups": ";Keyboard&Mouse",
"action": "Move",
"isComposite": false,
"isPartOfComposite": true
},
{
"name": "right",
"id": "77bff152-3580-4b21-b6de-dcd0c7e41164",
"path": "<Keyboard>/rightArrow",
"interactions": "",
"processors": "",
"groups": ";Keyboard&Mouse",
"action": "Move",
"isComposite": false,
"isPartOfComposite": true
},
{
"name": "",
"id": "1635d3fe-58b6-4ba9-a4e2-f4b964f6b5c8",
"path": "<XRController>/{Primary2DAxis}",
"interactions": "",
"processors": "",
"groups": "XR",
"action": "Move",
"isComposite": false,
"isPartOfComposite": false
},
{
"name": "",
"id": "3ea4d645-4504-4529-b061-ab81934c3752",
"path": "<Joystick>/stick",
"interactions": "",
"processors": "",
"groups": "Joystick",
"action": "Move",
"isComposite": false,
"isPartOfComposite": false
},
{
"name": "",
"id": "c1f7a91b-d0fd-4a62-997e-7fb9b69bf235",
"path": "<Gamepad>/rightStick",
"interactions": "",
"processors": "",
"groups": ";Gamepad",
"action": "Look",
"isComposite": false,
"isPartOfComposite": false
},
{
"name": "",
"id": "8c8e490b-c610-4785-884f-f04217b23ca4",
"path": "<Pointer>/delta",
"interactions": "",
"processors": "",
"groups": ";Keyboard&Mouse;Touch",
"action": "Look",
"isComposite": false,
"isPartOfComposite": false
},
{
"name": "",
"id": "3e5f5442-8668-4b27-a940-df99bad7e831",
"path": "<Joystick>/{Hatswitch}",
"interactions": "",
"processors": "",
"groups": "Joystick",
"action": "Look",
"isComposite": false,
"isPartOfComposite": false
},
{
"name": "",
"id": "143bb1cd-cc10-4eca-a2f0-a3664166fe91",
"path": "<Gamepad>/rightTrigger",
"interactions": "",
"processors": "",
"groups": ";Gamepad",
"action": "Fire",
"isComposite": false,
"isPartOfComposite": false
},
{
"name": "",
"id": "05f6913d-c316-48b2-a6bb-e225f14c7960",
"path": "<Mouse>/leftButton",
"interactions": "",
"processors": "",
"groups": ";Keyboard&Mouse",
"action": "Fire",
"isComposite": false,
"isPartOfComposite": false
},
{
"name": "",
"id": "886e731e-7071-4ae4-95c0-e61739dad6fd",
"path": "<Touchscreen>/primaryTouch/tap",
"interactions": "",
"processors": "",
"groups": ";Touch",
"action": "Fire",
"isComposite": false,
"isPartOfComposite": false
},
{
"name": "",
"id": "ee3d0cd2-254e-47a7-a8cb-bc94d9658c54",
"path": "<Joystick>/trigger",
"interactions": "",
"processors": "",
"groups": "Joystick",
"action": "Fire",
"isComposite": false,
"isPartOfComposite": false
},
{
"name": "",
"id": "8255d333-5683-4943-a58a-ccb207ff1dce",
"path": "<XRController>/{PrimaryAction}",
"interactions": "",
"processors": "",
"groups": "XR",
"action": "Fire",
"isComposite": false,
"isPartOfComposite": false
}
]
},
{
"name": "UI",
"id": "272f6d14-89ba-496f-b7ff-215263d3219f",
"actions": [
{
"name": "Navigate",
"type": "PassThrough",
"id": "c95b2375-e6d9-4b88-9c4c-c5e76515df4b",
"expectedControlType": "Vector2",
"processors": "",
"interactions": "",
"initialStateCheck": false
},
{
"name": "Submit",
"type": "Button",
"id": "7607c7b6-cd76-4816-beef-bd0341cfe950",
"expectedControlType": "Button",
"processors": "",
"interactions": "",
"initialStateCheck": false
},
{
"name": "Cancel",
"type": "Button",
"id": "15cef263-9014-4fd5-94d9-4e4a6234a6ef",
"expectedControlType": "Button",
"processors": "",
"interactions": "",
"initialStateCheck": false
},
{
"name": "Point",
"type": "PassThrough",
"id": "32b35790-4ed0-4e9a-aa41-69ac6d629449",
"expectedControlType": "Vector2",
"processors": "",
"interactions": "",
"initialStateCheck": true
},
{
"name": "Click",
"type": "PassThrough",
"id": "3c7022bf-7922-4f7c-a998-c437916075ad",
"expectedControlType": "Button",
"processors": "",
"interactions": "",
"initialStateCheck": true
},
{
"name": "ScrollWheel",
"type": "PassThrough",
"id": "0489e84a-4833-4c40-bfae-cea84b696689",
"expectedControlType": "Vector2",
"processors": "",
"interactions": "",
"initialStateCheck": false
},
{
"name": "MiddleClick",
"type": "PassThrough",
"id": "dad70c86-b58c-4b17-88ad-f5e53adf419e",
"expectedControlType": "Button",
"processors": "",
"interactions": "",
"initialStateCheck": false
},
{
"name": "RightClick",
"type": "PassThrough",
"id": "44b200b1-1557-4083-816c-b22cbdf77ddf",
"expectedControlType": "Button",
"processors": "",
"interactions": "",
"initialStateCheck": false
},
{
"name": "TrackedDevicePosition",
"type": "PassThrough",
"id": "24908448-c609-4bc3-a128-ea258674378a",
"expectedControlType": "Vector3",
"processors": "",
"interactions": "",
"initialStateCheck": false
},
{
"name": "TrackedDeviceOrientation",
"type": "PassThrough",
"id": "9caa3d8a-6b2f-4e8e-8bad-6ede561bd9be",
"expectedControlType": "Quaternion",
"processors": "",
"interactions": "",
"initialStateCheck": false
}
],
"bindings": [
{
"name": "Gamepad",
"id": "809f371f-c5e2-4e7a-83a1-d867598f40dd",
"path": "2DVector",
"interactions": "",
"processors": "",
"groups": "",
"action": "Navigate",
"isComposite": true,
"isPartOfComposite": false
},
{
"name": "up",
"id": "14a5d6e8-4aaf-4119-a9ef-34b8c2c548bf",
"path": "<Gamepad>/leftStick/up",
"interactions": "",
"processors": "",
"groups": ";Gamepad",
"action": "Navigate",
"isComposite": false,
"isPartOfComposite": true
},
{
"name": "up",
"id": "9144cbe6-05e1-4687-a6d7-24f99d23dd81",
"path": "<Gamepad>/rightStick/up",
"interactions": "",
"processors": "",
"groups": ";Gamepad",
"action": "Navigate",
"isComposite": false,
"isPartOfComposite": true
},
{
"name": "down",
"id": "2db08d65-c5fb-421b-983f-c71163608d67",
"path": "<Gamepad>/leftStick/down",
"interactions": "",
"processors": "",
"groups": ";Gamepad",
"action": "Navigate",
"isComposite": false,
"isPartOfComposite": true
},
{
"name": "down",
"id": "58748904-2ea9-4a80-8579-b500e6a76df8",
"path": "<Gamepad>/rightStick/down",
"interactions": "",
"processors": "",
"groups": ";Gamepad",
"action": "Navigate",
"isComposite": false,
"isPartOfComposite": true
},
{
"name": "left",
"id": "8ba04515-75aa-45de-966d-393d9bbd1c14",
"path": "<Gamepad>/leftStick/left",
"interactions": "",
"processors": "",
"groups": ";Gamepad",
"action": "Navigate",
"isComposite": false,
"isPartOfComposite": true
},
{
"name": "left",
"id": "712e721c-bdfb-4b23-a86c-a0d9fcfea921",
"path": "<Gamepad>/rightStick/left",
"interactions": "",
"processors": "",
"groups": ";Gamepad",
"action": "Navigate",
"isComposite": false,
"isPartOfComposite": true
},
{
"name": "right",
"id": "fcd248ae-a788-4676-a12e-f4d81205600b",
"path": "<Gamepad>/leftStick/right",
"interactions": "",
"processors": "",
"groups": ";Gamepad",
"action": "Navigate",
"isComposite": false,
"isPartOfComposite": true
},
{
"name": "right",
"id": "1f04d9bc-c50b-41a1-bfcc-afb75475ec20",
"path": "<Gamepad>/rightStick/right",
"interactions": "",
"processors": "",
"groups": ";Gamepad",
"action": "Navigate",
"isComposite": false,
"isPartOfComposite": true
},
{
"name": "",
"id": "fb8277d4-c5cd-4663-9dc7-ee3f0b506d90",
"path": "<Gamepad>/dpad",
"interactions": "",
"processors": "",
"groups": ";Gamepad",
"action": "Navigate",
"isComposite": false,
"isPartOfComposite": false
},
{
"name": "Joystick",
"id": "e25d9774-381c-4a61-b47c-7b6b299ad9f9",
"path": "2DVector",
"interactions": "",
"processors": "",
"groups": "",
"action": "Navigate",
"isComposite": true,
"isPartOfComposite": false
},
{
"name": "up",
"id": "3db53b26-6601-41be-9887-63ac74e79d19",
"path": "<Joystick>/stick/up",
"interactions": "",
"processors": "",
"groups": "Joystick",
"action": "Navigate",
"isComposite": false,
"isPartOfComposite": true
},
{
"name": "down",
"id": "0cb3e13e-3d90-4178-8ae6-d9c5501d653f",
"path": "<Joystick>/stick/down",
"interactions": "",
"processors": "",
"groups": "Joystick",
"action": "Navigate",
"isComposite": false,
"isPartOfComposite": true
},
{
"name": "left",
"id": "0392d399-f6dd-4c82-8062-c1e9c0d34835",
"path": "<Joystick>/stick/left",
"interactions": "",
"processors": "",
"groups": "Joystick",
"action": "Navigate",
"isComposite": false,
"isPartOfComposite": true
},
{
"name": "right",
"id": "942a66d9-d42f-43d6-8d70-ecb4ba5363bc",
"path": "<Joystick>/stick/right",
"interactions": "",
"processors": "",
"groups": "Joystick",
"action": "Navigate",
"isComposite": false,
"isPartOfComposite": true
},
{
"name": "Keyboard",
"id": "ff527021-f211-4c02-933e-5976594c46ed",
"path": "2DVector",
"interactions": "",
"processors": "",
"groups": "",
"action": "Navigate",
"isComposite": true,
"isPartOfComposite": false
},
{
"name": "up",
"id": "563fbfdd-0f09-408d-aa75-8642c4f08ef0",
"path": "<Keyboard>/w",
"interactions": "",
"processors": "",
"groups": "Keyboard&Mouse",
"action": "Navigate",
"isComposite": false,
"isPartOfComposite": true
},
{
"name": "up",
"id": "eb480147-c587-4a33-85ed-eb0ab9942c43",
"path": "<Keyboard>/upArrow",
"interactions": "",
"processors": "",
"groups": "Keyboard&Mouse",
"action": "Navigate",
"isComposite": false,
"isPartOfComposite": true
},
{
"name": "down",
"id": "2bf42165-60bc-42ca-8072-8c13ab40239b",
"path": "<Keyboard>/s",
"interactions": "",
"processors": "",
"groups": "Keyboard&Mouse",
"action": "Navigate",
"isComposite": false,
"isPartOfComposite": true
},
{
"name": "down",
"id": "85d264ad-e0a0-4565-b7ff-1a37edde51ac",
"path": "<Keyboard>/downArrow",
"interactions": "",
"processors": "",
"groups": "Keyboard&Mouse",
"action": "Navigate",
"isComposite": false,
"isPartOfComposite": true
},
{
"name": "left",
"id": "74214943-c580-44e4-98eb-ad7eebe17902",
"path": "<Keyboard>/a",
"interactions": "",
"processors": "",
"groups": "Keyboard&Mouse",
"action": "Navigate",
"isComposite": false,
"isPartOfComposite": true
},
{
"name": "left",
"id": "cea9b045-a000-445b-95b8-0c171af70a3b",
"path": "<Keyboard>/leftArrow",
"interactions": "",
"processors": "",
"groups": "Keyboard&Mouse",
"action": "Navigate",
"isComposite": false,
"isPartOfComposite": true
},
{
"name": "right",
"id": "8607c725-d935-4808-84b1-8354e29bab63",
"path": "<Keyboard>/d",
"interactions": "",
"processors": "",
"groups": "Keyboard&Mouse",
"action": "Navigate",
"isComposite": false,
"isPartOfComposite": true
},
{
"name": "right",
"id": "4cda81dc-9edd-4e03-9d7c-a71a14345d0b",
"path": "<Keyboard>/rightArrow",
"interactions": "",
"processors": "",
"groups": "Keyboard&Mouse",
"action": "Navigate",
"isComposite": false,
"isPartOfComposite": true
},
{
"name": "",
"id": "9e92bb26-7e3b-4ec4-b06b-3c8f8e498ddc",
"path": "*/{Submit}",
"interactions": "",
"processors": "",
"groups": "Keyboard&Mouse;Gamepad;Touch;Joystick;XR",
"action": "Submit",
"isComposite": false,
"isPartOfComposite": false
},
{
"name": "",
"id": "82627dcc-3b13-4ba9-841d-e4b746d6553e",
"path": "*/{Cancel}",
"interactions": "",
"processors": "",
"groups": "Keyboard&Mouse;Gamepad;Touch;Joystick;XR",
"action": "Cancel",
"isComposite": false,
"isPartOfComposite": false
},
{
"name": "",
"id": "c52c8e0b-8179-41d3-b8a1-d149033bbe86",
"path": "<Mouse>/position",
"interactions": "",
"processors": "",
"groups": "Keyboard&Mouse",
"action": "Point",
"isComposite": false,
"isPartOfComposite": false
},
{
"name": "",
"id": "e1394cbc-336e-44ce-9ea8-6007ed6193f7",
"path": "<Pen>/position",
"interactions": "",
"processors": "",
"groups": "Keyboard&Mouse",
"action": "Point",
"isComposite": false,
"isPartOfComposite": false
},
{
"name": "",
"id": "5693e57a-238a-46ed-b5ae-e64e6e574302",
"path": "<Touchscreen>/touch*/position",
"interactions": "",
"processors": "",
"groups": "Touch",
"action": "Point",
"isComposite": false,
"isPartOfComposite": false
},
{
"name": "",
"id": "4faf7dc9-b979-4210-aa8c-e808e1ef89f5",
"path": "<Mouse>/leftButton",
"interactions": "",
"processors": "",
"groups": ";Keyboard&Mouse",
"action": "Click",
"isComposite": false,
"isPartOfComposite": false
},
{
"name": "",
"id": "8d66d5ba-88d7-48e6-b1cd-198bbfef7ace",
"path": "<Pen>/tip",
"interactions": "",
"processors": "",
"groups": ";Keyboard&Mouse",
"action": "Click",
"isComposite": false,
"isPartOfComposite": false
},
{
"name": "",
"id": "47c2a644-3ebc-4dae-a106-589b7ca75b59",
"path": "<Touchscreen>/touch*/press",
"interactions": "",
"processors": "",
"groups": "Touch",
"action": "Click",
"isComposite": false,
"isPartOfComposite": false
},
{
"name": "",
"id": "bb9e6b34-44bf-4381-ac63-5aa15d19f677",
"path": "<XRController>/trigger",
"interactions": "",
"processors": "",
"groups": "XR",
"action": "Click",
"isComposite": false,
"isPartOfComposite": false
},
{
"name": "",
"id": "38c99815-14ea-4617-8627-164d27641299",
"path": "<Mouse>/scroll",
"interactions": "",
"processors": "",
"groups": ";Keyboard&Mouse",
"action": "ScrollWheel",
"isComposite": false,
"isPartOfComposite": false
},
{
"name": "",
"id": "24066f69-da47-44f3-a07e-0015fb02eb2e",
"path": "<Mouse>/middleButton",
"interactions": "",
"processors": "",
"groups": ";Keyboard&Mouse",
"action": "MiddleClick",
"isComposite": false,
"isPartOfComposite": false
},
{
"name": "",
"id": "4c191405-5738-4d4b-a523-c6a301dbf754",
"path": "<Mouse>/rightButton",
"interactions": "",
"processors": "",
"groups": ";Keyboard&Mouse",
"action": "RightClick",
"isComposite": false,
"isPartOfComposite": false
},
{
"name": "",
"id": "7236c0d9-6ca3-47cf-a6ee-a97f5b59ea77",
"path": "<XRController>/devicePosition",
"interactions": "",
"processors": "",
"groups": "XR",
"action": "TrackedDevicePosition",
"isComposite": false,
"isPartOfComposite": false
},
{
"name": "",
"id": "23e01e3a-f935-4948-8d8b-9bcac77714fb",
"path": "<XRController>/deviceRotation",
"interactions": "",
"processors": "",
"groups": "XR",
"action": "TrackedDeviceOrientation",
"isComposite": false,
"isPartOfComposite": false
}
]
}
],
"controlSchemes": [
{
"name": "Keyboard&Mouse",
"bindingGroup": "Keyboard&Mouse",
"devices": [
{
"devicePath": "<Keyboard>",
"isOptional": false,
"isOR": false
},
{
"devicePath": "<Mouse>",
"isOptional": false,
"isOR": false
}
]
},
{
"name": "Gamepad",
"bindingGroup": "Gamepad",
"devices": [
{
"devicePath": "<Gamepad>",
"isOptional": false,
"isOR": false
}
]
},
{
"name": "Touch",
"bindingGroup": "Touch",
"devices": [
{
"devicePath": "<Touchscreen>",
"isOptional": false,
"isOR": false
}
]
},
{
"name": "Joystick",
"bindingGroup": "Joystick",
"devices": [
{
"devicePath": "<Joystick>",
"isOptional": false,
"isOR": false
}
]
},
{
"name": "XR",
"bindingGroup": "XR",
"devices": [
{
"devicePath": "<XRController>",
"isOptional": false,
"isOR": false
}
]
}
]
}

View File

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

View File

@@ -0,0 +1,119 @@
using System;
using System.Diagnostics;
using UnityEngine.InputSystem.Controls;
////TODO: API to get the control and device from the internal context
////TODO: ToString()
namespace UnityEngine.InputSystem
{
/// <summary>
/// Wraps around values provided by input actions.
/// </summary>
/// <remarks>
/// This is a wrapper around <see cref="InputAction.CallbackContext"/> chiefly for use
/// with GameObject messages (i.e. <see cref="GameObject.SendMessage(string,object)"/>). It exists
/// so that action callback data can be represented as an object, can be reused, and shields
/// the receiver from having to know about action callback specifics.
/// </remarks>
/// <seealso cref="InputAction"/>
[DebuggerDisplay("Value = {Get()}")]
public class InputValue
{
/// <summary>
/// Read the current value as an object.
/// </summary>
/// <remarks>
/// This method allocates GC memory and will thus create garbage. If used during gameplay,
/// it will lead to GC spikes.
/// </remarks>
/// <returns>The current value in the form of a boxed object.</returns>
public object Get()
{
return m_Context.Value.ReadValueAsObject();
}
////TODO: add automatic conversions
/// <summary>
/// Read the current value of the action.
/// </summary>
/// <returns>The current value from the action cast to the specified type.</returns>
/// <typeparam name="TValue">Type of value to read. This must correspond to the
/// <see cref="InputControl.valueType"/> of the action or, if it is a composite, by the
/// <see cref="InputBindingComposite.valueType"/>.
/// The type depends on what type of controls the action is bound to.
/// Common types are <c>float</c> and <see cref="UnityEngine.Vector2"/></typeparam>
/// <exception cref="InvalidOperationException">The given type <typeparamref name="TValue"/>
/// does not match the value type expected by the control or binding composite.</exception>
/// <remarks>
/// The following example shows how to read a value from a <see cref="PlayerInput"/> message.
/// The given <c>InputValue</c> is only valid for the duration of the callback. Storing the <c>InputValue</c> references somewhere and calling Get&lt;T&gt;() later does not work correctly.
/// </remarks>
/// <example>
/// <code>
/// using UnityEngine;
/// using UnityEngine.InputSystem;
/// [RequireComponent(typeof(PlayerInput))]
/// public class MyPlayerLogic : MonoBehaviour
/// {
/// private Vector2 m_Move;
///
/// // 'Move' input action has been triggered.
/// public void OnMove(InputValue value)
/// {
/// // Read value from control. The type depends on what type of controls the action is bound to.
/// m_Move = value.Get&lt;Vector2&gt;();
/// }
///
/// public void Update()
/// {
/// // Update transform from m_Move
/// }
/// }
/// </code>
/// </example>
/// <seealso cref="InputAction.CallbackContext.ReadValue{TValue}"/>
public TValue Get<TValue>()
where TValue : struct
{
if (!m_Context.HasValue)
throw new InvalidOperationException($"Values can only be retrieved while in message callbacks");
return m_Context.Value.ReadValue<TValue>();
}
////TODO: proper message if value type isn't right
/// <summary>
/// Check if the action button is pressed.
/// </summary>
/// <remarks>
/// True if the button is activated over the button threshold. False otherwise
/// The following example check if a button is pressed when receiving a <see cref="PlayerInput"/> message.
/// The given <c>InputValue</c> is only valid for the duration of the callback. Storing the <c>InputValue</c> references somewhere and calling Get&lt;T&gt;() later does not work correctly.
/// </remarks>
/// <example>
/// <code>
/// [RequireComponent(typeof(PlayerInput))]
/// public class MyPlayerLogic : MonoBehaviour
/// {
/// // 'Fire' input action has been triggered.
/// public void OnFire(InputValue value)
/// {
/// if (value.isPressed)
/// FireWeapon();
/// }
///
/// public void FireWeapon()
/// {
/// // Weapon firing code
/// }
/// }
/// </code>
/// </example>
/// <seealso cref="ButtonControl.pressPointOrDefault"/>
public bool isPressed => Get<float>() >= ButtonControl.s_GlobalDefaultButtonPressPoint;
internal InputAction.CallbackContext? m_Context;
}
}

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 62899f850307741f2a39c98a8b639597
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: -100
icon: {fileID: 2800000, guid: 40265a896a0f341bb956e90c59025a29, type: 3}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,653 @@
#if UNITY_EDITOR
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using UnityEditor;
using UnityEngine.InputSystem.Users;
using UnityEngine.InputSystem.Utilities;
#if UNITY_INPUT_SYSTEM_ENABLE_UI
using UnityEngine.InputSystem.UI;
using UnityEngine.InputSystem.UI.Editor;
#endif
////TODO: detect if new input system isn't enabled and provide UI to enable it
#pragma warning disable 0414
namespace UnityEngine.InputSystem.Editor
{
/// <summary>
/// A custom inspector for the <see cref="PlayerInput"/> component.
/// </summary>
[CustomEditor(typeof(PlayerInput))]
internal class PlayerInputEditor : UnityEditor.Editor
{
public const string kDefaultInputActionsAssetPath =
"Packages/com.unity.inputsystem/InputSystem/Plugins/PlayerInput/DefaultInputActions.inputactions";
public void OnEnable()
{
InputActionImporter.onImport += Refresh;
InputUser.onChange += OnUserChange;
// Look up properties.
m_ActionsProperty = serializedObject.FindProperty(nameof(PlayerInput.m_Actions));
m_DefaultControlSchemeProperty = serializedObject.FindProperty(nameof(PlayerInput.m_DefaultControlScheme));
m_NeverAutoSwitchControlSchemesProperty = serializedObject.FindProperty(nameof(PlayerInput.m_NeverAutoSwitchControlSchemes));
m_DefaultActionMapProperty = serializedObject.FindProperty(nameof(PlayerInput.m_DefaultActionMap));
m_NotificationBehaviorProperty = serializedObject.FindProperty(nameof(PlayerInput.m_NotificationBehavior));
m_CameraProperty = serializedObject.FindProperty(nameof(PlayerInput.m_Camera));
m_ActionEventsProperty = serializedObject.FindProperty(nameof(PlayerInput.m_ActionEvents));
m_DeviceLostEventProperty = serializedObject.FindProperty(nameof(PlayerInput.m_DeviceLostEvent));
m_DeviceRegainedEventProperty = serializedObject.FindProperty(nameof(PlayerInput.m_DeviceRegainedEvent));
m_ControlsChangedEventProperty = serializedObject.FindProperty(nameof(PlayerInput.m_ControlsChangedEvent));
#if UNITY_INPUT_SYSTEM_ENABLE_UI
m_UIInputModuleProperty = serializedObject.FindProperty(nameof(PlayerInput.m_UIInputModule));
#endif
}
public void OnDisable()
{
new InputComponentEditorAnalytic(InputSystemComponent.PlayerInput).Send();
new PlayerInputEditorAnalytic(this).Send();
}
public void OnDestroy()
{
InputActionImporter.onImport -= Refresh;
InputUser.onChange -= OnUserChange;
}
private void Refresh()
{
////FIXME: doesn't seem like we're picking up the results of the latest import
m_ActionAssetInitialized = false;
Repaint();
}
private void OnUserChange(InputUser user, InputUserChange change, InputDevice device)
{
Repaint();
}
public override void OnInspectorGUI()
{
EditorGUI.BeginChangeCheck();
// Action config section.
EditorGUI.BeginChangeCheck();
EditorGUILayout.PropertyField(m_ActionsProperty);
var actionsWereChanged = false;
// Check for if we're using project-wide actions to raise a warning message.
if (m_ActionsProperty.objectReferenceValue != null)
{
InputActionAsset actions = m_ActionsProperty.objectReferenceValue as InputActionAsset;
if (actions == InputSystem.actions)
{
EditorGUILayout.HelpBox("Project-wide actions asset is not recommended to be used with Player " +
"Input because it is a singleton reference and all actions maps are enabled by default.\r\n" +
"You should manually disable all action maps on Start() and " +
"manually enable the default action map.",
MessageType.Warning);
}
}
var assetChanged = CheckIfActionAssetChanged();
// initialize the editor component if the asset has changed or if it has not been initialized yet
if (EditorGUI.EndChangeCheck() || !m_ActionAssetInitialized || assetChanged || m_ActionAssetInstanceID == 0)
{
InitializeEditorComponent(assetChanged);
actionsWereChanged = true;
}
++EditorGUI.indentLevel;
if (m_ControlSchemeOptions != null && m_ControlSchemeOptions.Length > 1) // Don't show if <Any> is the only option.
{
// Default control scheme picker.
Color currentBg = GUI.backgroundColor;
// if the invalid DefaultControlSchemeName is selected set the popup draw the BG color in red
if (m_InvalidDefaultControlSchemeName != null && m_SelectedDefaultControlScheme == 1)
GUI.backgroundColor = Color.red;
var rect = EditorGUILayout.GetControlRect();
var label = EditorGUI.BeginProperty(rect, m_DefaultControlSchemeText, m_DefaultControlSchemeProperty);
var selected = EditorGUI.Popup(rect, label, m_SelectedDefaultControlScheme, m_ControlSchemeOptions);
EditorGUI.EndProperty();
if (selected != m_SelectedDefaultControlScheme)
{
if (selected == 0)
{
m_DefaultControlSchemeProperty.stringValue = null;
}
// if there is an invalid default scheme name it will be at rank 1.
// we use m_InvalidDefaultControlSchemeName to prevent usage of the string with "name<Not Found>"
else if (m_InvalidDefaultControlSchemeName != null && selected == 1)
{
m_DefaultControlSchemeProperty.stringValue = m_InvalidDefaultControlSchemeName;
}
else
{
m_DefaultControlSchemeProperty.stringValue = m_ControlSchemeOptions[selected].text;
}
m_SelectedDefaultControlScheme = selected;
}
// Restore the initial color
GUI.backgroundColor = currentBg;
rect = EditorGUILayout.GetControlRect();
label = EditorGUI.BeginProperty(rect, m_AutoSwitchText, m_NeverAutoSwitchControlSchemesProperty);
var neverAutoSwitchValueOld = m_NeverAutoSwitchControlSchemesProperty.boolValue;
var neverAutoSwitchValueNew = !EditorGUI.Toggle(rect, label, !neverAutoSwitchValueOld);
EditorGUI.EndProperty();
if (neverAutoSwitchValueOld != neverAutoSwitchValueNew)
{
m_NeverAutoSwitchControlSchemesProperty.boolValue = neverAutoSwitchValueNew;
serializedObject.ApplyModifiedProperties();
}
}
if (m_ActionMapOptions != null && m_ActionMapOptions.Length > 0)
{
// Default action map picker.
var rect = EditorGUILayout.GetControlRect();
var label = EditorGUI.BeginProperty(rect, m_DefaultActionMapText, m_DefaultActionMapProperty);
var selected = EditorGUI.Popup(rect, label, m_SelectedDefaultActionMap,
m_ActionMapOptions);
EditorGUI.EndProperty();
if (selected != m_SelectedDefaultActionMap)
{
if (selected == 0)
{
m_DefaultActionMapProperty.stringValue = null;
}
else
{
// Use ID rather than name.
var asset = (InputActionAsset)m_ActionsProperty.objectReferenceValue;
var actionMap = asset.FindActionMap(m_ActionMapOptions[selected].text);
if (actionMap != null)
m_DefaultActionMapProperty.stringValue = actionMap.id.ToString();
}
m_SelectedDefaultActionMap = selected;
}
}
--EditorGUI.indentLevel;
DoHelpCreateAssetUI();
#if UNITY_INPUT_SYSTEM_ENABLE_UI
// UI config section.
if (m_UIPropertyText == null)
m_UIPropertyText = EditorGUIUtility.TrTextContent("UI Input Module", m_UIInputModuleProperty.GetTooltip());
EditorGUI.BeginChangeCheck();
EditorGUILayout.PropertyField(m_UIInputModuleProperty, m_UIPropertyText);
if (EditorGUI.EndChangeCheck())
serializedObject.ApplyModifiedProperties();
if (m_UIInputModuleProperty.objectReferenceValue != null)
{
var uiModule = m_UIInputModuleProperty.objectReferenceValue as InputSystemUIInputModule;
if (m_ActionsProperty.objectReferenceValue != null && uiModule.actionsAsset != m_ActionsProperty.objectReferenceValue)
{
EditorGUILayout.HelpBox("The referenced InputSystemUIInputModule is configured using different input actions than this PlayerInput. They should match if you want to synchronize PlayerInput actions to the UI input.", MessageType.Warning);
if (GUILayout.Button(m_FixInputModuleText))
InputSystemUIInputModuleEditor.ReassignActions(uiModule, m_ActionsProperty.objectReferenceValue as InputActionAsset);
}
}
#endif
// Camera section.
if (m_CameraPropertyText == null)
m_CameraPropertyText = EditorGUIUtility.TrTextContent("Camera", m_CameraProperty.GetTooltip());
EditorGUI.BeginChangeCheck();
EditorGUILayout.PropertyField(m_CameraProperty, m_CameraPropertyText);
if (EditorGUI.EndChangeCheck())
serializedObject.ApplyModifiedProperties();
// Notifications/event section.
EditorGUI.BeginChangeCheck();
EditorGUILayout.PropertyField(m_NotificationBehaviorProperty, m_NotificationBehaviorText);
if (EditorGUI.EndChangeCheck() || actionsWereChanged || !m_NotificationBehaviorInitialized)
OnNotificationBehaviorChange();
switch ((PlayerNotifications)m_NotificationBehaviorProperty.intValue)
{
case PlayerNotifications.SendMessages:
case PlayerNotifications.BroadcastMessages:
Debug.Assert(m_SendMessagesHelpText != null);
EditorGUILayout.HelpBox(m_SendMessagesHelpText);
break;
case PlayerNotifications.InvokeUnityEvents:
m_EventsGroupUnfolded = EditorGUILayout.Foldout(m_EventsGroupUnfolded, m_EventsGroupText, toggleOnLabelClick: true);
if (m_EventsGroupUnfolded)
{
// Action events. Group by action map.
if (m_ActionNames != null)
{
using (new EditorGUI.IndentLevelScope())
{
for (var n = 0; n < m_NumActionMaps; ++n)
{
// Skip action maps that have no names (case 1317735).
if (m_ActionMapNames[n] == null)
continue;
m_ActionMapEventsUnfolded[n] = EditorGUILayout.Foldout(m_ActionMapEventsUnfolded[n],
m_ActionMapNames[n], toggleOnLabelClick: true);
using (new EditorGUI.IndentLevelScope())
{
if (m_ActionMapEventsUnfolded[n])
{
for (var i = 0; i < m_ActionNames.Length; ++i)
{
if (m_ActionMapIndices[i] != n)
continue;
EditorGUILayout.PropertyField(m_ActionEventsProperty.GetArrayElementAtIndex(i), m_ActionNames[i]);
}
}
}
}
}
}
// Misc events.
EditorGUILayout.PropertyField(m_DeviceLostEventProperty);
EditorGUILayout.PropertyField(m_DeviceRegainedEventProperty);
EditorGUILayout.PropertyField(m_ControlsChangedEventProperty);
}
break;
}
// Miscellaneous buttons.
DoUtilityButtonsUI();
if (EditorGUI.EndChangeCheck())
serializedObject.ApplyModifiedProperties();
// Debug UI.
if (EditorApplication.isPlaying)
DoDebugUI();
}
// This checks changes that are not captured by BeginChangeCheck/EndChangeCheck.
// One such case is when the user triggers a "Reset" on the component.
bool CheckIfActionAssetChanged()
{
if (m_ActionsProperty.objectReferenceValue != null)
{
var assetInstanceID = m_ActionsProperty.objectReferenceValue.GetInstanceID();
// if the m_ActionAssetInstanceID is 0 the PlayerInputEditor has not been initialized yet, but the asset did not change
bool result = assetInstanceID != m_ActionAssetInstanceID && m_ActionAssetInstanceID != 0;
m_ActionAssetInstanceID = (int)assetInstanceID;
return result;
}
m_ActionAssetInstanceID = -1;
return false;
}
private void DoHelpCreateAssetUI()
{
if (m_ActionsProperty.objectReferenceValue != null)
{
// All good. We already have an asset.
return;
}
EditorGUILayout.HelpBox("There are no input actions associated with this input component yet. Click the button below to create "
+ "a new set of input actions or drag an existing input actions asset into the field above.", MessageType.Info);
EditorGUILayout.BeginHorizontal();
EditorGUILayout.Space();
if (GUILayout.Button(m_CreateActionsText, EditorStyles.miniButton, GUILayout.MaxWidth(120)))
{
// Request save file location.
var defaultFileName = Application.productName;
var fileName = EditorUtility.SaveFilePanel("Create Input Actions Asset", "Assets", defaultFileName,
InputActionAsset.Extension);
////TODO: take current Supported Devices into account when creating this
// Create and import asset and open editor.
if (!string.IsNullOrEmpty(fileName))
{
if (!fileName.StartsWith(Application.dataPath))
{
Debug.LogError($"Path must be located in Assets/ folder (got: '{fileName}')");
EditorGUILayout.EndHorizontal();
return;
}
if (!fileName.EndsWith("." + InputActionAsset.Extension))
fileName += "." + InputActionAsset.Extension;
// Load default actions and update all GUIDs.
var defaultActionsText = File.ReadAllText(kDefaultInputActionsAssetPath);
var newActions = InputActionAsset.FromJson(defaultActionsText);
foreach (var map in newActions.actionMaps)
{
map.m_Id = Guid.NewGuid().ToString();
foreach (var action in map.actions)
action.m_Id = Guid.NewGuid().ToString();
}
newActions.name = Path.GetFileNameWithoutExtension(fileName);
var newActionsText = newActions.ToJson();
// Write it out and tell the asset DB to pick it up.
File.WriteAllText(fileName, newActionsText);
// Import the new asset
var relativePath = "Assets/" + fileName.Substring(Application.dataPath.Length + 1);
AssetDatabase.ImportAsset(relativePath, ImportAssetOptions.ForceSynchronousImport);
// Load imported object.
var importedObject = AssetDatabase.LoadAssetAtPath<InputActionAsset>(relativePath);
// Set it on the PlayerInput component.
m_ActionsProperty.objectReferenceValue = importedObject;
serializedObject.ApplyModifiedProperties();
// Open the asset.
AssetDatabase.OpenAsset(importedObject);
}
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.Separator();
}
private void DoUtilityButtonsUI()
{
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button(m_OpenSettingsText, EditorStyles.miniButton))
InputSettingsProvider.Open();
if (GUILayout.Button(m_OpenDebuggerText, EditorStyles.miniButton))
InputDebuggerWindow.CreateOrShow();
EditorGUILayout.EndHorizontal();
}
private void DoDebugUI()
{
var playerInput = (PlayerInput)target;
if (!playerInput.user.valid)
return;
////TODO: show actions when they happen
var user = playerInput.user.index.ToString();
var controlScheme = playerInput.user.controlScheme?.name;
var devices = string.Join(", ", playerInput.user.pairedDevices);
EditorGUILayout.Space();
EditorGUILayout.LabelField(m_DebugText, EditorStyles.boldLabel);
EditorGUI.BeginDisabledGroup(true);
EditorGUILayout.LabelField("User", user);
EditorGUILayout.LabelField("Control Scheme", controlScheme);
EditorGUILayout.LabelField("Devices", devices);
EditorGUI.EndDisabledGroup();
}
private void OnNotificationBehaviorChange()
{
Debug.Assert(m_ActionAssetInitialized);
serializedObject.ApplyModifiedProperties();
var notificationBehavior = (PlayerNotifications)m_NotificationBehaviorProperty.intValue;
switch (notificationBehavior)
{
// Create text that lists all the messages sent by the component.
case PlayerNotifications.BroadcastMessages:
case PlayerNotifications.SendMessages:
{
var builder = new StringBuilder();
builder.Append("Will ");
if (notificationBehavior == PlayerNotifications.BroadcastMessages)
builder.Append("BroadcastMessage()");
else
builder.Append("SendMessage()");
builder.Append(" to GameObject: ");
builder.Append(PlayerInput.DeviceLostMessage);
builder.Append(", ");
builder.Append(PlayerInput.DeviceRegainedMessage);
builder.Append(", ");
builder.Append(PlayerInput.ControlsChangedMessage);
var playerInput = (PlayerInput)target;
var asset = playerInput.m_Actions;
if (asset != null)
{
foreach (var action in asset)
{
builder.Append(", On");
builder.Append(CSharpCodeHelpers.MakeTypeName(action.name));
}
}
m_SendMessagesHelpText = new GUIContent(builder.ToString());
break;
}
case PlayerNotifications.InvokeUnityEvents:
{
var playerInput = (PlayerInput)target;
bool areEventsDirty = (playerInput.m_DeviceLostEvent == null) || (playerInput.m_DeviceRegainedEvent == null) || (playerInput.m_ControlsChangedEvent == null);
playerInput.m_DeviceLostEvent ??= new PlayerInput.DeviceLostEvent();
playerInput.m_DeviceRegainedEvent ??= new PlayerInput.DeviceRegainedEvent();
playerInput.m_ControlsChangedEvent ??= new PlayerInput.ControlsChangedEvent();
if (areEventsDirty)
{
serializedObject.Update();
// Force action refresh.
m_ActionAssetInitialized = false;
Refresh();
}
break;
}
}
m_NotificationBehaviorInitialized = true;
}
private void InitializeEditorComponent(bool assetChanged)
{
serializedObject.ApplyModifiedProperties();
m_ActionAssetInitialized = true;
var playerInput = (PlayerInput)target;
var asset = (InputActionAsset)m_ActionsProperty.objectReferenceValue;
if (assetChanged)
m_SelectedDefaultActionMap = -1;
if (asset == null)
{
m_ControlSchemeOptions = null;
m_ActionMapOptions = null;
m_ActionNames = null;
m_SelectedDefaultControlScheme = -1;
m_InvalidDefaultControlSchemeName = null;
return;
}
// If we're sending Unity events, read out the event list.
if ((PlayerNotifications)m_NotificationBehaviorProperty.intValue ==
PlayerNotifications.InvokeUnityEvents)
{
////FIXME: this should preserve the same order that we have in the asset
var newActionNames = new List<GUIContent>();
var newActionEvents = new List<PlayerInput.ActionEvent>();
var newActionMapIndices = new List<int>();
m_NumActionMaps = 0;
m_ActionMapNames = null;
void AddEntry(InputAction action, PlayerInput.ActionEvent actionEvent)
{
newActionNames.Add(new GUIContent(action.name));
newActionEvents.Add(actionEvent);
var actionMapIndex = asset.actionMaps.IndexOfReference(action.actionMap);
newActionMapIndices.Add(actionMapIndex);
if (actionMapIndex >= m_NumActionMaps)
m_NumActionMaps = actionMapIndex + 1;
ArrayHelpers.PutAtIfNotSet(ref m_ActionMapNames, actionMapIndex,
() => new GUIContent(action.actionMap.name));
}
// Bring over any action events that we already have and that are still in the asset.
var oldActionEvents = playerInput.m_ActionEvents;
if (oldActionEvents != null)
{
foreach (var entry in oldActionEvents)
{
var guid = entry.actionId;
var action = asset.FindAction(guid);
if (action != null)
AddEntry(action, entry);
}
}
// Add any new actions.
foreach (var action in asset)
{
// Skip if it was already in there.
if (oldActionEvents != null && oldActionEvents.Any(x => x.actionId == action.id.ToString()))
continue;
////FIXME: adds bindings to the name
AddEntry(action, new PlayerInput.ActionEvent(action.id, action.ToString()));
}
m_ActionNames = newActionNames.ToArray();
m_ActionMapIndices = newActionMapIndices.ToArray();
Array.Resize(ref m_ActionMapEventsUnfolded, m_NumActionMaps);
playerInput.m_ActionEvents = newActionEvents.ToArray();
}
// Read out control schemes.
var selectedDefaultControlScheme = playerInput.defaultControlScheme;
m_InvalidDefaultControlSchemeName = null;
m_SelectedDefaultControlScheme = 0;
////TODO: sort alphabetically and ensure that the order is the same in the schemes editor
var controlSchemesNames = asset.controlSchemes.Select(cs => cs.name).ToList();
// try to find the selected Default Control Scheme
if (!string.IsNullOrEmpty(selectedDefaultControlScheme))
{
// +1 since <Any> will be the first in the list
m_SelectedDefaultControlScheme = 1 + controlSchemesNames.FindIndex(name => string.Compare(name, selectedDefaultControlScheme,
StringComparison.InvariantCultureIgnoreCase) == 0);
// if not found, will insert the invalid name next to <Any>
if (m_SelectedDefaultControlScheme == 0)
{
m_InvalidDefaultControlSchemeName = selectedDefaultControlScheme;
m_SelectedDefaultControlScheme = 1;
controlSchemesNames.Insert(0, $"{selectedDefaultControlScheme}{L10n.Tr("<Not Found>")}");
}
}
else
{
playerInput.defaultControlScheme = null;
}
m_ControlSchemeOptions = new GUIContent[controlSchemesNames.Count + 1];
m_ControlSchemeOptions[0] = new GUIContent(EditorGUIUtility.TrTextContent("<Any>"));
for (var i = 0; i < controlSchemesNames.Count; ++i)
{
m_ControlSchemeOptions[i + 1] = new GUIContent(controlSchemesNames[i]);
}
// Read out action maps.
var selectedDefaultActionMap = !string.IsNullOrEmpty(playerInput.defaultActionMap)
? asset.FindActionMap(playerInput.defaultActionMap)
: null;
m_SelectedDefaultActionMap = (asset.actionMaps.Count > 0 && m_SelectedDefaultActionMap == -1) ? 1 : 0;
var actionMaps = asset.actionMaps;
m_ActionMapOptions = new GUIContent[actionMaps.Count + 1];
m_ActionMapOptions[0] = new GUIContent(EditorGUIUtility.TrTextContent("<None>"));
////TODO: sort alphabetically
for (var i = 0; i < actionMaps.Count; ++i)
{
var actionMap = actionMaps[i];
m_ActionMapOptions[i + 1] = new GUIContent(actionMap.name);
if (selectedDefaultActionMap != null && actionMap == selectedDefaultActionMap)
m_SelectedDefaultActionMap = i + 1;
}
if (m_SelectedDefaultActionMap <= 0)
playerInput.defaultActionMap = null;
else
playerInput.defaultActionMap = m_ActionMapOptions[m_SelectedDefaultActionMap].text;
serializedObject.Update();
}
[SerializeField] private bool m_EventsGroupUnfolded;
[SerializeField] private bool[] m_ActionMapEventsUnfolded;
[NonSerialized] private readonly GUIContent m_CreateActionsText = EditorGUIUtility.TrTextContent("Create Actions...");
[NonSerialized] private readonly GUIContent m_FixInputModuleText = EditorGUIUtility.TrTextContent("Fix UI Input Module");
[NonSerialized] private readonly GUIContent m_OpenSettingsText = EditorGUIUtility.TrTextContent("Open Input Settings");
[NonSerialized] private readonly GUIContent m_OpenDebuggerText = EditorGUIUtility.TrTextContent("Open Input Debugger");
[NonSerialized] private readonly GUIContent m_EventsGroupText =
EditorGUIUtility.TrTextContent("Events", "UnityEvents triggered by the PlayerInput component");
[NonSerialized] private readonly GUIContent m_NotificationBehaviorText =
EditorGUIUtility.TrTextContent("Behavior",
"Determine how notifications should be sent when an input-related event associated with the player happens.");
[NonSerialized] private readonly GUIContent m_DefaultControlSchemeText =
EditorGUIUtility.TrTextContent("Default Scheme", "Which control scheme to try by default. If not set, PlayerInput "
+ "will simply go through all control schemes in the action asset and try one after the other. If set, PlayerInput will try "
+ "the given scheme first but if using that fails (e.g. when not required devices are missing) will fall back to trying the other "
+ "control schemes in order.");
[NonSerialized] private readonly GUIContent m_DefaultActionMapText =
EditorGUIUtility.TrTextContent("Default Map", "Action map to enable by default. If not set, no actions will be enabled by default.");
[NonSerialized] private readonly GUIContent m_AutoSwitchText =
EditorGUIUtility.TrTextContent("Auto-Switch",
"By default, when there is only a single PlayerInput, the player "
+ "is allowed to freely switch between control schemes simply by starting to use a different device. By toggling this property off, this "
+ "behavior is disabled and even with a single player, the player will stay locked onto the explicitly selected control scheme. Note "
+ "that you can still change control schemes explicitly through the PlayerInput API.\n\nWhen there are multiple PlayerInputs in the game, auto-switching is disabled automatically regardless of the value of this property.");
[NonSerialized] private readonly GUIContent m_DebugText = EditorGUIUtility.TrTextContent("Debug");
[NonSerialized] private GUIContent m_UIPropertyText;
[NonSerialized] private GUIContent m_CameraPropertyText;
[NonSerialized] private GUIContent m_SendMessagesHelpText;
[NonSerialized] private GUIContent[] m_ActionNames;
[NonSerialized] private GUIContent[] m_ActionMapNames;
[NonSerialized] private int[] m_ActionMapIndices;
[NonSerialized] private int m_NumActionMaps;
[NonSerialized] private int m_SelectedDefaultControlScheme;
[NonSerialized] private string m_InvalidDefaultControlSchemeName;
[NonSerialized] private GUIContent[] m_ControlSchemeOptions;
[NonSerialized] private int m_SelectedDefaultActionMap;
[NonSerialized] private GUIContent[] m_ActionMapOptions;
[NonSerialized] private SerializedProperty m_ActionsProperty;
[NonSerialized] private SerializedProperty m_DefaultControlSchemeProperty;
[NonSerialized] private SerializedProperty m_DefaultActionMapProperty;
[NonSerialized] private SerializedProperty m_NeverAutoSwitchControlSchemesProperty;
[NonSerialized] private SerializedProperty m_NotificationBehaviorProperty;
#if UNITY_INPUT_SYSTEM_ENABLE_UI
[NonSerialized] private SerializedProperty m_UIInputModuleProperty;
#endif
[NonSerialized] private SerializedProperty m_ActionEventsProperty;
[NonSerialized] private SerializedProperty m_CameraProperty;
[NonSerialized] private SerializedProperty m_DeviceLostEventProperty;
[NonSerialized] private SerializedProperty m_DeviceRegainedEventProperty;
[NonSerialized] private SerializedProperty m_ControlsChangedEventProperty;
[NonSerialized] private bool m_NotificationBehaviorInitialized;
[NonSerialized] private bool m_ActionAssetInitialized;
[NonSerialized] private int m_ActionAssetInstanceID;
}
}
#endif // UNITY_EDITOR

View File

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

View File

@@ -0,0 +1,813 @@
using System;
using UnityEngine.Events;
using UnityEngine.InputSystem.Controls;
using UnityEngine.InputSystem.LowLevel;
using UnityEngine.InputSystem.Users;
using UnityEngine.InputSystem.Utilities;
#if UNITY_EDITOR
using UnityEditor;
#endif
////REVIEW: should we automatically pool/retain up to maxPlayerCount player instances?
////REVIEW: the join/leave messages should probably give a *GameObject* rather than the PlayerInput component (which can be gotten to via a simple GetComponent(InChildren) call)
////TODO: add support for reacting to players missing devices
namespace UnityEngine.InputSystem
{
/// <summary>
/// Manages joining and leaving of players.
/// </summary>
/// <remarks>
/// This is a singleton component. Only one instance should be active in a game at any one time. To retrieve the
/// current instance, use the <see cref="instance"/> property.
///
/// PlayerInputManager provides the implementation of specific player joining mechanisms (<see cref="joinBehavior"/>).
/// It also automatically assigns <see cref="splitScreen">split-screen areas</see>. The input system does not require
/// the PlayerInputManager to have multiple <see cref="PlayerInput"/> components. However, you can always implement
/// your own custom logic instead and simply instantiate multiple [GameObjects](xref:UnityEngine.GameObject) with
/// <see cref="PlayerInput"/> yourself.
///
/// When you use PlayerInputManager, the join behavior you define controls pairing devices to players. This means
/// that <see cref="PlayerInput"/> automatically pairs the device from which the player joined. If control schemes
/// are present in the PlayerInput's set of <see cref="PlayerInput.actions"/>, the input system selects the first compatible
/// device for pairing. If additional devices are required, the input system selects them from the pool of currently
/// unpaired devices.
/// </remarks>
[AddComponentMenu("Input/Player Input Manager")]
[HelpURL(InputSystem.kDocUrl + "/manual/PlayerInputManager.html")]
public class PlayerInputManager : MonoBehaviour
{
/// <summary>
/// Name of the message that is sent when a player joins the game.
/// </summary>
public const string PlayerJoinedMessage = "OnPlayerJoined";
public const string PlayerLeftMessage = "OnPlayerLeft";
/// <summary>
/// If enabled, each player will automatically be assigned a portion of the available screen area.
/// </summary>
/// <remarks>
/// For this to work, each <see cref="PlayerInput"/> component must have an associated <see cref="Camera"/>
/// object through <see cref="PlayerInput.camera"/>.
///
/// Note that as player join, the screen may be increasingly subdivided and players may see their
/// previous screen area getting resized.
/// </remarks>
public bool splitScreen
{
get => m_SplitScreen;
set
{
if (m_SplitScreen == value)
return;
m_SplitScreen = value;
if (!m_SplitScreen)
{
// Reset rects on all player cameras.
foreach (var player in PlayerInput.all)
{
var camera = player.camera;
if (camera != null)
camera.rect = new Rect(0, 0, 1, 1);
}
}
else
{
UpdateSplitScreen();
}
}
}
////REVIEW: we probably need support for filling unused screen areas automatically
/// <summary>
/// If <see cref="splitScreen"/> is enabled, this property determines whether subdividing the screen is allowed to
/// produce screen areas that have an aspect ratio different from the screen resolution.
/// </summary>
/// <remarks>
/// By default, when <see cref="splitScreen"/> is enabled, the manager will add or remove screen subdivisions in
/// steps of two. This means that when, for example, the second player is added, the screen will be subdivided into
/// a left and a right screen area; the left one allocated to the first player and the right one allocated to the
/// second player.
///
/// This behavior makes optimal use of screen real estate but will result in screen areas that have aspect ratios
/// different from the screen resolution. If this is not acceptable, this property can be set to true to enforce
/// split-screen to only create screen areas that have the same aspect ratio of the screen.
///
/// This results in the screen being subdivided more aggressively. When, for example, a second player is added,
/// the screen will immediately be divided into a four-way split-screen setup with the lower two screen areas
/// not being used.
///
/// This property is irrelevant if <see cref="fixedNumberOfSplitScreens"/> is used.
/// </remarks>
public bool maintainAspectRatioInSplitScreen => m_MaintainAspectRatioInSplitScreen;
/// <summary>
/// If <see cref="splitScreen"/> is enabled, this property determines how many screen divisions there will be.
/// </summary>
/// <remarks>
/// This is only used if <see cref="splitScreen"/> is true.
///
/// By default this is set to -1 which means the screen will automatically be divided to best fit the
/// current number of players i.e. the highest player index in <see cref="PlayerInput"/>
/// </remarks>
public int fixedNumberOfSplitScreens => m_FixedNumberOfSplitScreens;
/// <summary>
/// The normalized screen rectangle available for allocating player split-screens into.
/// </summary>
/// <remarks>
/// This is only used if <see cref="splitScreen"/> is true.
///
/// By default it is set to <c>(0,0,1,1)</c>, i.e. the entire screen area will be used for player screens.
/// If, for example, part of the screen should display a UI/information shared by all players, this
/// property can be used to cut off the area and not have it used by PlayerInputManager.
/// </remarks>
public Rect splitScreenArea => m_SplitScreenRect;
/// <summary>
/// The current number of active players.
/// </summary>
/// <remarks>
/// This count corresponds to all <see cref="PlayerInput"/> instances that are currently enabled.
/// </remarks>
public int playerCount => PlayerInput.s_AllActivePlayersCount;
////FIXME: this needs to be settable
/// <summary>
/// Maximum number of players allowed concurrently in the game.
/// </summary>
/// <remarks>
/// If this limit is reached, joining is turned off automatically.
///
/// By default this is set to -1. Any negative value deactivates the player limit and allows
/// arbitrary many players to join.
/// </remarks>
public int maxPlayerCount => m_MaxPlayerCount;
/// <summary>
/// Whether new players can currently join.
/// </summary>
/// <remarks>
/// While this is true, new players can join via the mechanism determined by <see cref="joinBehavior"/>.
/// </remarks>
/// <seealso cref="EnableJoining"/>
/// <seealso cref="DisableJoining"/>
public bool joiningEnabled => m_AllowJoining;
/// <summary>
/// Determines the mechanism by which players can join when joining is enabled (<see cref="joiningEnabled"/>).
/// </summary>
/// <remarks>
/// </remarks>
public PlayerJoinBehavior joinBehavior
{
get => m_JoinBehavior;
set
{
if (m_JoinBehavior == value)
return;
var joiningEnabled = m_AllowJoining;
if (joiningEnabled)
DisableJoining();
m_JoinBehavior = value;
if (joiningEnabled)
EnableJoining();
}
}
/// <summary>
/// The input action that a player must trigger to join the game.
/// </summary>
/// <remarks>
/// If the join action is a reference to an existing input action, it will be cloned when the PlayerInputManager
/// is enabled. This avoids the situation where the join action can become disabled after the first user joins which
/// can happen when the join action is the same as a player in-game action. When a player joins, input bindings from
/// devices other than the device they joined with are disabled. If the join action had a binding for keyboard and one
/// for gamepad for example, and the first player joined using the keyboard, the expectation is that the next player
/// could still join by pressing the gamepad join button. Without the cloning behavior, the gamepad input would have
/// been disabled.
///
/// For more details about joining behavior, see <see cref="PlayerInput"/>.
/// </remarks>
public InputActionProperty joinAction
{
get => m_JoinAction;
set
{
if (m_JoinAction == value)
return;
////REVIEW: should we suppress notifications for temporary disables?
var joinEnabled = m_AllowJoining && m_JoinBehavior == PlayerJoinBehavior.JoinPlayersWhenJoinActionIsTriggered;
if (joinEnabled)
DisableJoining();
m_JoinAction = value;
if (joinEnabled)
EnableJoining();
}
}
public PlayerNotifications notificationBehavior
{
get => m_NotificationBehavior;
set => m_NotificationBehavior = value;
}
public PlayerJoinedEvent playerJoinedEvent
{
get
{
if (m_PlayerJoinedEvent == null)
m_PlayerJoinedEvent = new PlayerJoinedEvent();
return m_PlayerJoinedEvent;
}
}
public PlayerLeftEvent playerLeftEvent
{
get
{
if (m_PlayerLeftEvent == null)
m_PlayerLeftEvent = new PlayerLeftEvent();
return m_PlayerLeftEvent;
}
}
public event Action<PlayerInput> onPlayerJoined
{
add
{
if (value == null)
throw new ArgumentNullException(nameof(value));
m_PlayerJoinedCallbacks.AddCallback(value);
}
remove
{
if (value == null)
throw new ArgumentNullException(nameof(value));
m_PlayerJoinedCallbacks.RemoveCallback(value);
}
}
public event Action<PlayerInput> onPlayerLeft
{
add
{
if (value == null)
throw new ArgumentNullException(nameof(value));
m_PlayerLeftCallbacks.AddCallback(value);
}
remove
{
if (value == null)
throw new ArgumentNullException(nameof(value));
m_PlayerLeftCallbacks.RemoveCallback(value);
}
}
/// <summary>
/// Reference to the prefab that the manager will instantiate when players join.
/// </summary>
/// <value>Prefab to instantiate for new players.</value>
public GameObject playerPrefab
{
get => m_PlayerPrefab;
set => m_PlayerPrefab = value;
}
/// <summary>
/// Singleton instance of the manager.
/// </summary>
/// <value>Singleton instance or null.</value>
public static PlayerInputManager instance { get; private set; }
/// <summary>
/// Allow players to join the game based on <see cref="joinBehavior"/>.
/// </summary>
/// <seealso cref="DisableJoining"/>
/// <seealso cref="joiningEnabled"/>
public void EnableJoining()
{
switch (m_JoinBehavior)
{
case PlayerJoinBehavior.JoinPlayersWhenButtonIsPressed:
ValidateInputActionAsset();
if (!m_UnpairedDeviceUsedDelegateHooked)
{
if (m_UnpairedDeviceUsedDelegate == null)
m_UnpairedDeviceUsedDelegate = OnUnpairedDeviceUsed;
InputUser.onUnpairedDeviceUsed += m_UnpairedDeviceUsedDelegate;
m_UnpairedDeviceUsedDelegateHooked = true;
++InputUser.listenForUnpairedDeviceActivity;
}
break;
case PlayerJoinBehavior.JoinPlayersWhenJoinActionIsTriggered:
// Hook into join action if we have one.
if (m_JoinAction.action != null)
{
if (!m_JoinActionDelegateHooked)
{
if (m_JoinActionDelegate == null)
m_JoinActionDelegate = JoinPlayerFromActionIfNotAlreadyJoined;
m_JoinAction.action.performed += m_JoinActionDelegate;
m_JoinActionDelegateHooked = true;
}
m_JoinAction.action.Enable();
}
else
{
Debug.LogError(
$"No join action configured on PlayerInputManager but join behavior is set to {nameof(PlayerJoinBehavior.JoinPlayersWhenJoinActionIsTriggered)}",
this);
}
break;
}
m_AllowJoining = true;
}
/// <summary>
/// Inhibit players from joining the game.
/// </summary>
/// <remarks>
/// Note that this method might disable the action, depending on how the player
/// joined initially. Specifically, if the initial joining was triggered using
/// the <see cref="PlayerJoinBehavior.JoinPlayersWhenJoinActionIsTriggered"/> behavior,
/// this method also disables the join action.
/// </remarks>
/// <seealso cref="EnableJoining"/>
/// <seealso cref="joiningEnabled"/>
public void DisableJoining()
{
switch (m_JoinBehavior)
{
case PlayerJoinBehavior.JoinPlayersWhenButtonIsPressed:
if (m_UnpairedDeviceUsedDelegateHooked)
{
InputUser.onUnpairedDeviceUsed -= m_UnpairedDeviceUsedDelegate;
m_UnpairedDeviceUsedDelegateHooked = false;
--InputUser.listenForUnpairedDeviceActivity;
}
break;
case PlayerJoinBehavior.JoinPlayersWhenJoinActionIsTriggered:
if (m_JoinActionDelegateHooked)
{
var joinAction = m_JoinAction.action;
if (joinAction != null)
m_JoinAction.action.performed -= m_JoinActionDelegate;
m_JoinActionDelegateHooked = false;
}
m_JoinAction.action?.Disable();
break;
}
m_AllowJoining = false;
}
////TODO
/// <summary>
/// Join a new player based on input on a UI element.
/// </summary>
/// <remarks>
/// This should be called directly from a UI callback such as <see cref="Button.onClick"/>. The device
/// that the player joins with is taken from the device that was used to interact with the UI element.
/// </remarks>
internal void JoinPlayerFromUI()
{
if (!CheckIfPlayerCanJoin())
return;
//find used device; InputSystemUIInputModule should probably make that available
throw new NotImplementedException();
}
/// <summary>
/// Join a new player based on input received through an <see cref="InputAction"/>.
/// </summary>
/// <param name="context"></param>
/// <remarks>
/// </remarks>
public void JoinPlayerFromAction(InputAction.CallbackContext context)
{
if (!CheckIfPlayerCanJoin())
return;
var device = context.control.device;
JoinPlayer(pairWithDevice: device);
}
public void JoinPlayerFromActionIfNotAlreadyJoined(InputAction.CallbackContext context)
{
if (!CheckIfPlayerCanJoin())
return;
var device = context.control.device;
if (PlayerInput.FindFirstPairedToDevice(device) != null)
return;
JoinPlayer(pairWithDevice: device);
}
/// <summary>
/// Spawn a new player from <see cref="playerPrefab"/>.
/// </summary>
/// <param name="playerIndex">Optional explicit <see cref="PlayerInput.playerIndex"/> to assign to the player. Must be unique within
/// <see cref="PlayerInput.all"/>. If not supplied, a player index will be assigned automatically (smallest unused index will be used).</param>
/// <param name="splitScreenIndex">Optional <see cref="PlayerInput.splitScreenIndex"/>. If supplied, this assigns a split-screen area to the player. For example,
/// a split-screen index of </param>
/// <param name="controlScheme">Control scheme to activate on the player (optional). If not supplied, a control scheme will
/// be selected based on <paramref name="pairWithDevice"/>. If no device is given either, the first control scheme that matches
/// the currently available unpaired devices (see <see cref="InputUser.GetUnpairedInputDevices()"/>) is used.</param>
/// <param name="pairWithDevice">Device to pair to the player. Also determines which control scheme to use if <paramref name="controlScheme"/>
/// is not given.</param>
/// <returns>The newly instantiated player or <c>null</c> if joining failed.</returns>
/// <remarks>
/// Joining must be enabled (see <see cref="joiningEnabled"/>) or the method will fail.
///
/// To pair multiple devices, use <see cref="JoinPlayer(int,int,string,InputDevice[])"/>.
/// </remarks>
public PlayerInput JoinPlayer(int playerIndex = -1, int splitScreenIndex = -1, string controlScheme = null, InputDevice pairWithDevice = null)
{
if (!CheckIfPlayerCanJoin(playerIndex))
return null;
PlayerInput.s_DestroyIfDeviceSetupUnsuccessful = true;
return PlayerInput.Instantiate(m_PlayerPrefab, playerIndex: playerIndex, splitScreenIndex: splitScreenIndex,
controlScheme: controlScheme, pairWithDevice: pairWithDevice);
}
/// <summary>
/// Spawn a new player from <see cref="playerPrefab"/>.
/// </summary>
/// <param name="playerIndex">Optional explicit <see cref="PlayerInput.playerIndex"/> to assign to the player. Must be unique within
/// <see cref="PlayerInput.all"/>. If not supplied, a player index will be assigned automatically (smallest unused index will be used).</param>
/// <param name="splitScreenIndex">Optional <see cref="PlayerInput.splitScreenIndex"/>. If supplied, this assigns a split-screen area to the player. For example,
/// a split-screen index of </param>
/// <param name="controlScheme">Control scheme to activate on the player (optional). If not supplied, a control scheme will
/// be selected based on <paramref name="pairWithDevices"/>. If no device is given either, the first control scheme that matches
/// the currently available unpaired devices (see <see cref="InputUser.GetUnpairedInputDevices()"/>) is used.</param>
/// <param name="pairWithDevices">Devices to pair to the player. Also determines which control scheme to use if <paramref name="controlScheme"/>
/// is not given.</param>
/// <returns>The newly instantiated player or <c>null</c> if joining failed.</returns>
/// <remarks>
/// Joining must be enabled (see <see cref="joiningEnabled"/>) or the method will fail.
/// </remarks>
public PlayerInput JoinPlayer(int playerIndex = -1, int splitScreenIndex = -1, string controlScheme = null, params InputDevice[] pairWithDevices)
{
if (!CheckIfPlayerCanJoin(playerIndex))
return null;
PlayerInput.s_DestroyIfDeviceSetupUnsuccessful = true;
return PlayerInput.Instantiate(m_PlayerPrefab, playerIndex: playerIndex, splitScreenIndex: splitScreenIndex,
controlScheme: controlScheme, pairWithDevices: pairWithDevices);
}
[SerializeField] internal PlayerNotifications m_NotificationBehavior;
[Tooltip("Set a limit for the maximum number of players who are able to join.")]
[SerializeField] internal int m_MaxPlayerCount = -1;
[SerializeField] internal bool m_AllowJoining = true;
[SerializeField] internal PlayerJoinBehavior m_JoinBehavior;
[SerializeField] internal PlayerJoinedEvent m_PlayerJoinedEvent;
[SerializeField] internal PlayerLeftEvent m_PlayerLeftEvent;
[SerializeField] internal InputActionProperty m_JoinAction;
[SerializeField] internal GameObject m_PlayerPrefab;
[SerializeField] internal bool m_SplitScreen;
[SerializeField] internal bool m_MaintainAspectRatioInSplitScreen;
[Tooltip("Explicitly set a fixed number of screens or otherwise allow the screen to be divided automatically to best fit the number of players.")]
[SerializeField] internal int m_FixedNumberOfSplitScreens = -1;
[SerializeField] internal Rect m_SplitScreenRect = new Rect(0, 0, 1, 1);
[NonSerialized] private bool m_JoinActionDelegateHooked;
[NonSerialized] private bool m_UnpairedDeviceUsedDelegateHooked;
[NonSerialized] private Action<InputAction.CallbackContext> m_JoinActionDelegate;
[NonSerialized] private Action<InputControl, InputEventPtr> m_UnpairedDeviceUsedDelegate;
[NonSerialized] private CallbackArray<Action<PlayerInput>> m_PlayerJoinedCallbacks;
[NonSerialized] private CallbackArray<Action<PlayerInput>> m_PlayerLeftCallbacks;
internal static string[] messages => new[]
{
PlayerJoinedMessage,
PlayerLeftMessage,
};
private bool CheckIfPlayerCanJoin(int playerIndex = -1)
{
if (m_PlayerPrefab == null)
{
Debug.LogError("playerPrefab must be set in order to be able to join new players", this);
return false;
}
if (m_MaxPlayerCount >= 0 && playerCount >= m_MaxPlayerCount)
{
Debug.LogWarning("Maximum number of supported players reached: " + maxPlayerCount, this);
return false;
}
// If we have a player index, make sure it's unique.
if (playerIndex != -1)
{
for (var i = 0; i < PlayerInput.s_AllActivePlayersCount; ++i)
if (PlayerInput.s_AllActivePlayers[i].playerIndex == playerIndex)
{
Debug.LogError(
$"Player index #{playerIndex} is already taken by player {PlayerInput.s_AllActivePlayers[i]}",
PlayerInput.s_AllActivePlayers[i]);
return false;
}
}
return true;
}
private void OnUnpairedDeviceUsed(InputControl control, InputEventPtr eventPtr)
{
if (!m_AllowJoining)
return;
if (m_JoinBehavior == PlayerJoinBehavior.JoinPlayersWhenButtonIsPressed)
{
// Make sure it's a button that was actuated.
if (!(control is ButtonControl))
return;
// Make sure it's a device that is usable by the player's actions. We don't want
// to join a player who's then stranded and has no way to actually interact with the game.
if (!IsDeviceUsableWithPlayerActions(control.device))
return;
////REVIEW: should we log a warning or error when the actions for the player do not have control schemes?
JoinPlayer(pairWithDevice: control.device);
}
}
private void OnEnable()
{
if (instance == null)
{
instance = this;
}
else
{
Debug.LogWarning("Multiple PlayerInputManagers in the game. There should only be one PlayerInputManager", this);
return;
}
// if the join action is a reference, clone it so we don't run into problems with the action being disabled by
// PlayerInput when devices are assigned to individual players
if (joinAction.reference != null && joinAction.action?.actionMap?.asset != null)
{
var inputActionAsset = Instantiate(joinAction.action.actionMap.asset);
var inputActionReference = InputActionReference.Create(inputActionAsset.FindAction(joinAction.action.name));
joinAction = new InputActionProperty(inputActionReference);
}
// Join all players already in the game.
for (var i = 0; i < PlayerInput.s_AllActivePlayersCount; ++i)
NotifyPlayerJoined(PlayerInput.s_AllActivePlayers[i]);
if (m_AllowJoining)
EnableJoining();
}
private void OnDisable()
{
if (instance == this)
instance = null;
if (m_AllowJoining)
DisableJoining();
}
/// <summary>
/// If split-screen is enabled, then for each player in the game, adjust the player's <see cref="Camera.rect"/>
/// to fit the player's split screen area according to the number of players currently in the game and the
/// current split-screen configuration.
/// </summary>
private void UpdateSplitScreen()
{
// Nothing to do if split-screen is not enabled.
if (!m_SplitScreen)
return;
// Determine number of split-screens to create based on highest player index we have.
var minSplitScreenCount = 0;
foreach (var player in PlayerInput.all)
{
if (player.playerIndex >= minSplitScreenCount)
minSplitScreenCount = player.playerIndex + 1;
}
// Adjust to fixed number if we have it.
if (m_FixedNumberOfSplitScreens > 0)
{
if (m_FixedNumberOfSplitScreens < minSplitScreenCount)
Debug.LogWarning(
$"Highest playerIndex of {minSplitScreenCount} exceeds fixed number of split-screens of {m_FixedNumberOfSplitScreens}",
this);
minSplitScreenCount = m_FixedNumberOfSplitScreens;
}
// Determine divisions along X and Y. Usually, we have a square grid of split-screens so all we need to
// do is make it large enough to fit all players.
var numDivisionsX = Mathf.CeilToInt(Mathf.Sqrt(minSplitScreenCount));
var numDivisionsY = numDivisionsX;
if (!m_MaintainAspectRatioInSplitScreen && numDivisionsX * (numDivisionsX - 1) >= minSplitScreenCount)
{
// We're allowed to produce split-screens with aspect ratios different from the screen meaning
// that we always add one more column before finally adding an entirely new row.
numDivisionsY -= 1;
}
// Assign split-screen area to each player.
foreach (var player in PlayerInput.all)
{
// Make sure the player's splitScreenIndex isn't out of range.
var splitScreenIndex = player.splitScreenIndex;
if (splitScreenIndex >= numDivisionsX * numDivisionsY)
{
Debug.LogError(
$"Split-screen index of {splitScreenIndex} on player is out of range (have {numDivisionsX * numDivisionsY} screens); resetting to playerIndex",
player);
player.m_SplitScreenIndex = player.playerIndex;
}
// Make sure we have a camera.
var camera = player.camera;
if (camera == null)
{
Debug.LogError(
"Player has no camera associated with it. Cannot set up split-screen. Point PlayerInput.camera to camera for player.",
player);
continue;
}
// Assign split-screen area based on m_SplitScreenRect.
var column = splitScreenIndex % numDivisionsX;
var row = splitScreenIndex / numDivisionsX;
var rect = new Rect
{
width = m_SplitScreenRect.width / numDivisionsX,
height = m_SplitScreenRect.height / numDivisionsY
};
rect.x = m_SplitScreenRect.x + column * rect.width;
// Y is bottom-to-top but we fill from top down.
rect.y = m_SplitScreenRect.y + m_SplitScreenRect.height - (row + 1) * rect.height;
camera.rect = rect;
}
}
private bool IsDeviceUsableWithPlayerActions(InputDevice device)
{
Debug.Assert(device != null);
if (m_PlayerPrefab == null)
return true;
var playerInput = m_PlayerPrefab.GetComponentInChildren<PlayerInput>();
if (playerInput == null)
return true;
var actions = playerInput.actions;
if (actions == null)
return true;
// If the asset has control schemes, see if there's one that works with the device plus
// whatever unpaired devices we have left.
if (actions.controlSchemes.Count > 0)
{
using (var unpairedDevices = InputUser.GetUnpairedInputDevices())
{
if (InputControlScheme.FindControlSchemeForDevices(unpairedDevices, actions.controlSchemes,
mustIncludeDevice: device) == null)
return false;
}
return true;
}
// Otherwise just check whether any of the maps has bindings usable with the device.
foreach (var actionMap in actions.actionMaps)
if (actionMap.IsUsableWithDevice(device))
return true;
return false;
}
private void ValidateInputActionAsset()
{
#if DEVELOPMENT_BUILD || UNITY_EDITOR
if (m_PlayerPrefab == null || m_PlayerPrefab.GetComponentInChildren<PlayerInput>() == null)
return;
var actions = m_PlayerPrefab.GetComponentInChildren<PlayerInput>().actions;
if (actions == null)
return;
var isValid = true;
foreach (var controlScheme in actions.controlSchemes)
{
if (controlScheme.deviceRequirements.Count > 0)
break;
isValid = false;
}
if (isValid) return;
var assetInfo = actions.name;
#if UNITY_EDITOR
assetInfo = AssetDatabase.GetAssetPath(actions);
#endif
Debug.LogWarning($"The input action asset '{assetInfo}' in the player prefab assigned to PlayerInputManager has " +
"no control schemes with required devices. The JoinPlayersWhenButtonIsPressed join behavior " +
"will not work unless the expected input devices are listed as requirements in the input " +
"action asset.", m_PlayerPrefab);
#endif
}
/// <summary>
/// Called by <see cref="PlayerInput"/> when it is enabled.
/// </summary>
/// <param name="player"></param>
internal void NotifyPlayerJoined(PlayerInput player)
{
Debug.Assert(player != null);
UpdateSplitScreen();
switch (m_NotificationBehavior)
{
case PlayerNotifications.SendMessages:
SendMessage(PlayerJoinedMessage, player, SendMessageOptions.DontRequireReceiver);
break;
case PlayerNotifications.BroadcastMessages:
BroadcastMessage(PlayerJoinedMessage, player, SendMessageOptions.DontRequireReceiver);
break;
case PlayerNotifications.InvokeUnityEvents:
m_PlayerJoinedEvent?.Invoke(player);
break;
case PlayerNotifications.InvokeCSharpEvents:
DelegateHelpers.InvokeCallbacksSafe(ref m_PlayerJoinedCallbacks, player, "onPlayerJoined");
break;
}
}
/// <summary>
/// Called by <see cref="PlayerInput"/> when it is disabled.
/// </summary>
/// <param name="player"></param>
internal void NotifyPlayerLeft(PlayerInput player)
{
Debug.Assert(player != null);
UpdateSplitScreen();
switch (m_NotificationBehavior)
{
case PlayerNotifications.SendMessages:
SendMessage(PlayerLeftMessage, player, SendMessageOptions.DontRequireReceiver);
break;
case PlayerNotifications.BroadcastMessages:
BroadcastMessage(PlayerLeftMessage, player, SendMessageOptions.DontRequireReceiver);
break;
case PlayerNotifications.InvokeUnityEvents:
m_PlayerLeftEvent?.Invoke(player);
break;
case PlayerNotifications.InvokeCSharpEvents:
DelegateHelpers.InvokeCallbacksSafe(ref m_PlayerLeftCallbacks, player, "onPlayerLeft");
break;
}
}
[Serializable]
public class PlayerJoinedEvent : UnityEvent<PlayerInput>
{
}
[Serializable]
public class PlayerLeftEvent : UnityEvent<PlayerInput>
{
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 621567455fd1c4ceb811cc8a00b6a1a5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 40265a896a0f341bb956e90c59025a29, type: 3}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,269 @@
#if UNITY_EDITOR
using System;
using System.Linq;
using UnityEditor;
using UnityEngine.InputSystem.Users;
namespace UnityEngine.InputSystem.Editor
{
/// <summary>
/// Custom inspector for <see cref="PlayerInputManager"/>.
/// </summary>
[CustomEditor(typeof(PlayerInputManager))]
internal class PlayerInputManagerEditor : UnityEditor.Editor
{
public void OnEnable()
{
InputUser.onChange += OnUserChange;
}
public void OnDisable()
{
new InputComponentEditorAnalytic(InputSystemComponent.PlayerInputManager).Send();
new PlayerInputManagerEditorAnalytic(this).Send();
}
public void OnDestroy()
{
InputUser.onChange -= OnUserChange;
}
private void OnUserChange(InputUser user, InputUserChange change, InputDevice device)
{
Repaint();
}
public override void OnInspectorGUI()
{
////TODO: cache properties
EditorGUI.BeginChangeCheck();
DoNotificationSectionUI();
EditorGUILayout.Space();
DoJoinSectionUI();
EditorGUILayout.Space();
DoSplitScreenSectionUI();
if (EditorGUI.EndChangeCheck())
serializedObject.ApplyModifiedProperties();
if (EditorApplication.isPlaying)
DoDebugUI();
}
private void DoNotificationSectionUI()
{
var notificationBehaviorProperty = serializedObject.FindProperty(nameof(PlayerInputManager.m_NotificationBehavior));
EditorGUILayout.PropertyField(notificationBehaviorProperty);
switch ((PlayerNotifications)notificationBehaviorProperty.intValue)
{
case PlayerNotifications.SendMessages:
if (m_SendMessagesHelpText == null)
m_SendMessagesHelpText = EditorGUIUtility.TrTextContent(
$"Will SendMessage() to GameObject: " + string.Join(",", PlayerInputManager.messages));
EditorGUILayout.HelpBox(m_SendMessagesHelpText);
break;
case PlayerNotifications.BroadcastMessages:
if (m_BroadcastMessagesHelpText == null)
m_BroadcastMessagesHelpText = EditorGUIUtility.TrTextContent(
$"Will BroadcastMessage() to GameObject: " + string.Join(",", PlayerInputManager.messages));
EditorGUILayout.HelpBox(m_BroadcastMessagesHelpText);
break;
case PlayerNotifications.InvokeUnityEvents:
m_EventsExpanded = EditorGUILayout.Foldout(m_EventsExpanded, m_EventsLabel, toggleOnLabelClick: true);
if (m_EventsExpanded)
{
var playerJoinedEventProperty = serializedObject.FindProperty(nameof(PlayerInputManager.m_PlayerJoinedEvent));
var playerLeftEventProperty = serializedObject.FindProperty(nameof(PlayerInputManager.m_PlayerLeftEvent));
EditorGUILayout.PropertyField(playerJoinedEventProperty);
EditorGUILayout.PropertyField(playerLeftEventProperty);
}
break;
}
}
private void DoJoinSectionUI()
{
EditorGUILayout.LabelField(m_JoiningGroupLabel, EditorStyles.boldLabel);
// Join behavior
var joinBehaviorProperty = serializedObject.FindProperty(nameof(PlayerInputManager.m_JoinBehavior));
EditorGUILayout.PropertyField(joinBehaviorProperty);
if ((PlayerJoinBehavior)joinBehaviorProperty.intValue != PlayerJoinBehavior.JoinPlayersManually)
{
++EditorGUI.indentLevel;
// Join action.
if ((PlayerJoinBehavior)joinBehaviorProperty.intValue ==
PlayerJoinBehavior.JoinPlayersWhenJoinActionIsTriggered)
{
var joinActionProperty = serializedObject.FindProperty(nameof(PlayerInputManager.m_JoinAction));
EditorGUILayout.PropertyField(joinActionProperty);
}
// Player prefab.
var playerPrefabProperty = serializedObject.FindProperty(nameof(PlayerInputManager.m_PlayerPrefab));
EditorGUILayout.PropertyField(playerPrefabProperty);
ValidatePlayerPrefab(joinBehaviorProperty, playerPrefabProperty);
--EditorGUI.indentLevel;
}
// Enabled-by-default.
var allowJoiningProperty = serializedObject.FindProperty(nameof(PlayerInputManager.m_AllowJoining));
if (m_AllowingJoiningLabel == null)
m_AllowingJoiningLabel = new GUIContent("Joining Enabled By Default", allowJoiningProperty.GetTooltip());
EditorGUILayout.PropertyField(allowJoiningProperty, m_AllowingJoiningLabel);
// Max player count.
var maxPlayerCountProperty = serializedObject.FindProperty(nameof(PlayerInputManager.m_MaxPlayerCount));
if (m_EnableMaxPlayerCountLabel == null)
m_EnableMaxPlayerCountLabel = EditorGUIUtility.TrTextContent("Limit Number of Players", maxPlayerCountProperty.GetTooltip());
if (maxPlayerCountProperty.intValue > 0)
m_MaxPlayerCountEnabled = true;
m_MaxPlayerCountEnabled = EditorGUILayout.Toggle(m_EnableMaxPlayerCountLabel, m_MaxPlayerCountEnabled);
if (m_MaxPlayerCountEnabled)
{
++EditorGUI.indentLevel;
if (maxPlayerCountProperty.intValue < 0)
maxPlayerCountProperty.intValue = 1;
EditorGUILayout.PropertyField(maxPlayerCountProperty);
--EditorGUI.indentLevel;
}
else
maxPlayerCountProperty.intValue = -1;
}
private static void ValidatePlayerPrefab(SerializedProperty joinBehaviorProperty,
SerializedProperty playerPrefabProperty)
{
if ((PlayerJoinBehavior)joinBehaviorProperty.intValue != PlayerJoinBehavior.JoinPlayersWhenButtonIsPressed)
return;
if (playerPrefabProperty.objectReferenceValue == null)
return;
var playerInput = ((GameObject)playerPrefabProperty.objectReferenceValue)
.GetComponentInChildren<PlayerInput>();
if (playerInput == null)
{
EditorGUILayout.HelpBox("No PlayerInput component found in player prefab.", MessageType.Info);
return;
}
if (playerInput.actions == null)
{
EditorGUILayout.HelpBox("PlayerInput component has no input action asset assigned.", MessageType.Info);
return;
}
if (playerInput.actions.controlSchemes.Any(c => c.deviceRequirements.Count > 0) == false)
EditorGUILayout.HelpBox("Join Players When Button Is Pressed behavior will not work when the Input Action Asset " +
"assigned to the PlayerInput component has no required devices in any control scheme.",
MessageType.Info);
}
private void DoSplitScreenSectionUI()
{
EditorGUILayout.LabelField(m_SplitScreenGroupLabel, EditorStyles.boldLabel);
// Split-screen toggle.
var splitScreenProperty = serializedObject.FindProperty(nameof(PlayerInputManager.m_SplitScreen));
if (m_SplitScreenLabel == null)
m_SplitScreenLabel = new GUIContent("Enable Split-Screen", splitScreenProperty.GetTooltip());
EditorGUILayout.PropertyField(splitScreenProperty, m_SplitScreenLabel);
if (!splitScreenProperty.boolValue)
return;
++EditorGUI.indentLevel;
// Maintain-aspect-ratio toggle.
var maintainAspectRatioProperty = serializedObject.FindProperty(nameof(PlayerInputManager.m_MaintainAspectRatioInSplitScreen));
if (m_MaintainAspectRatioLabel == null)
m_MaintainAspectRatioLabel =
new GUIContent("Maintain Aspect Ratio", maintainAspectRatioProperty.GetTooltip());
EditorGUILayout.PropertyField(maintainAspectRatioProperty, m_MaintainAspectRatioLabel);
// Fixed-number toggle.
var fixedNumberProperty = serializedObject.FindProperty(nameof(PlayerInputManager.m_FixedNumberOfSplitScreens));
if (m_EnableFixedNumberOfSplitScreensLabel == null)
m_EnableFixedNumberOfSplitScreensLabel = EditorGUIUtility.TrTextContent("Set Fixed Number", fixedNumberProperty.GetTooltip());
if (fixedNumberProperty.intValue > 0)
m_FixedNumberOfSplitScreensEnabled = true;
m_FixedNumberOfSplitScreensEnabled = EditorGUILayout.Toggle(m_EnableFixedNumberOfSplitScreensLabel,
m_FixedNumberOfSplitScreensEnabled);
if (m_FixedNumberOfSplitScreensEnabled)
{
++EditorGUI.indentLevel;
if (fixedNumberProperty.intValue < 0)
fixedNumberProperty.intValue = 4;
if (m_FixedNumberOfSplitScreensLabel == null)
m_FixedNumberOfSplitScreensLabel = EditorGUIUtility.TrTextContent("Number of Screens",
fixedNumberProperty.tooltip);
EditorGUILayout.PropertyField(fixedNumberProperty, m_FixedNumberOfSplitScreensLabel);
--EditorGUI.indentLevel;
}
else
{
fixedNumberProperty.intValue = -1;
}
// Split-screen area.
var splitScreenAreaProperty = serializedObject.FindProperty(nameof(PlayerInputManager.m_SplitScreenRect));
if (m_SplitScreenAreaLabel == null)
m_SplitScreenAreaLabel = new GUIContent("Screen Rectangle", splitScreenAreaProperty.GetTooltip());
EditorGUILayout.PropertyField(splitScreenAreaProperty, m_SplitScreenAreaLabel);
--EditorGUI.indentLevel;
}
private void DoDebugUI()
{
EditorGUILayout.Space();
EditorGUILayout.LabelField(m_DebugLabel, EditorStyles.boldLabel);
EditorGUI.BeginDisabledGroup(true);
var players = PlayerInput.all;
if (players.Count == 0)
{
EditorGUILayout.LabelField("No Players");
}
else
{
foreach (var player in players)
{
var str = player.gameObject.name;
if (player.splitScreenIndex != -1)
str += $" (Screen #{player.splitScreenIndex})";
EditorGUILayout.LabelField("Player #" + player.playerIndex, str);
}
}
EditorGUI.EndDisabledGroup();
}
[SerializeField] private bool m_EventsExpanded;
[SerializeField] private bool m_MaxPlayerCountEnabled;
[SerializeField] private bool m_FixedNumberOfSplitScreensEnabled;
[NonSerialized] private readonly GUIContent m_JoiningGroupLabel = EditorGUIUtility.TrTextContent("Joining");
[NonSerialized] private readonly GUIContent m_SplitScreenGroupLabel = EditorGUIUtility.TrTextContent("Split-Screen");
[NonSerialized] private readonly GUIContent m_EventsLabel = EditorGUIUtility.TrTextContent("Events");
[NonSerialized] private readonly GUIContent m_DebugLabel = EditorGUIUtility.TrTextContent("Debug");
[NonSerialized] private GUIContent m_SendMessagesHelpText;
[NonSerialized] private GUIContent m_BroadcastMessagesHelpText;
[NonSerialized] private GUIContent m_AllowingJoiningLabel;
[NonSerialized] private GUIContent m_SplitScreenLabel;
[NonSerialized] private GUIContent m_MaintainAspectRatioLabel;
[NonSerialized] private GUIContent m_SplitScreenAreaLabel;
[NonSerialized] private GUIContent m_FixedNumberOfSplitScreensLabel;
[NonSerialized] private GUIContent m_EnableMaxPlayerCountLabel;
[NonSerialized] private GUIContent m_EnableFixedNumberOfSplitScreensLabel;
}
}
#endif // UNITY_EDITOR

View File

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

View File

@@ -0,0 +1,40 @@
namespace UnityEngine.InputSystem
{
/// <summary>
/// Determines how <see cref="PlayerInputManager"/> joins new players.
/// </summary>
/// <remarks>
/// </remarks>
/// <seealso cref="PlayerInputManager"/>
/// <seealso cref="PlayerInputManager.joinBehavior"/>
public enum PlayerJoinBehavior
{
/// <summary>
/// Listen for button presses on devices that are not paired to any player. If they occur
/// and joining is allowed, join a new player using the device the button was pressed on.
/// </summary>
JoinPlayersWhenButtonIsPressed,
/// <summary>
/// Listen for button presses on devices that are not paired to any player. If the control
/// they triggered matches a specific action and joining is allowed, join a new player using
/// the device the button was pressed on.
/// </summary>
JoinPlayersWhenJoinActionIsTriggered,
/// <summary>
/// Don't join players automatically. Call <see cref="PlayerInputManager.JoinPlayerFromUI"/>
/// or <see cref="PlayerInputManager.JoinPlayerFromAction"/> explicitly in order to join new
/// players. Alternatively, just create GameObjects with <see cref="PlayerInput"/>
/// components directly and they will be joined automatically.
/// </summary>
/// <remarks>
/// This behavior also allows implementing more sophisticated device pairing mechanisms when multiple devices
/// are involved. While initial engagement required by <see cref="JoinPlayersWhenButtonIsPressed"/> or
/// <see cref="JoinPlayersWhenJoinActionIsTriggered"/> allows pairing a single device reliably to a player,
/// additional devices that may be required by a control scheme will still get paired automatically out of the
/// pool of available devices.
/// </remarks>
JoinPlayersManually,
}
}

View File

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

View File

@@ -0,0 +1,44 @@
namespace UnityEngine.InputSystem
{
/// <summary>
/// Determines how the triggering of an action or other input-related events are relayed to other GameObjects.
/// </summary>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1717:Only FlagsAttribute enums should have plural names")]
public enum PlayerNotifications
{
////TODO: add a "None" behavior; for actions, users may want to poll (or use the generated interfaces)
/// <summary>
/// Use <see cref="GameObject.SendMessage(string,object)"/> to send a message to the <see cref="GameObject"/>
/// that <see cref="PlayerInput"/> belongs to.
///
/// The message name will be the name of the action (e.g. "Jump"; it will not include the action map name),
/// and the object will be the <see cref="PlayerInput"/> on which the action was triggered.
///
/// If the notification is for an action that was triggered, <see cref="SendMessageOptions"/> will be
/// <see cref="SendMessageOptions.RequireReceiver"/> (i.e. an error will be logged if there is no corresponding
/// method). Otherwise it will be <see cref="SendMessageOptions.DontRequireReceiver"/>.
/// </summary>
SendMessages,
/// <summary>
/// Like <see cref="SendMessages"/> but instead of using <see cref="GameObject.SendMessage(string,object)"/>,
/// use <see cref="GameObject.BroadcastMessage(string,object)"/>.
/// </summary>
BroadcastMessages,
/// <summary>
/// Have a separate <a href="https://docs.unity3d.com/ScriptReference/Events.UnityEvent.html">UnityEvent</a> for each notification.
/// Allows wiring up target methods to invoke such that the connection is persisted in Unity serialized data.
///
/// See <see cref="PlayerInput.actionEvents"/> and related callbacks such as <see cref="PlayerInput.controlsChangedEvent"/>.
/// </summary>
InvokeUnityEvents,
////TODO: Kill
/// <summary>
/// Use plain C# callbacks.
/// </summary>
InvokeCSharpEvents
}
}

View File

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