如果已经了解过基本概念了,请当我前面没写,直接跳至使用方法
在unity中除了传统的通过Input来获取按键输入的方式外
还有一种新的控制输入方法,那就是Input system
但网上的记录感觉还是太少了,感觉不太能满足需求,所以自己就做了一点总结,来防止自己忘记
input system 主要完成的工作是作为一个中间层
将按键响应包装为事件响应,从而方便自己进行管理
易于使用和快速设置,从而方便快速添加基本控件。
当然,并不是说旧的不好,如果能够满足使用,完全没必要再刻意去替换,当然如果想支持新的设备,可以考虑使用input system
安装的过程就省略了,网上太多且简单
input system文件可以直接通过create进行创建( Assets > Create > Input Actions),其内部如下
用于定义用户控制游戏时使用的不同类型的设备或一组设备
比如:
- 键盘鼠标
- 游戏手柄
- 方向盘等
可以通过设置多种类型的Control Schemes保持不同类型的控制系统分离
当然也可以不用,只用添加不同的Action,然后绑定好控制
使用多个Control Schemes的目的是,方便游戏切换设备操作模式,比如单人/双人模式
又或者不同设备的控制
Action Maps 包含特定类型行为的所有动作。
比如:你可以把所有角色的通用动作放在一起,像移动,跳跃,开火等
一个Action Maps 可以取名为Player 或者 Gameplay 只要你喜欢
那么就带来一个问题,似乎我只需要一个Action Maps就够了,那为什么要多个呢?
比如,你做了一个第一人称的游戏,包含射击和驾驶的功能
这时,你需要一个Action Maps去控制人物的移动和射击,但同时你又需要另一个Action Maps 用于驾驶控制
玩游戏多的人都知道,这两种操作之间很可能会发生按键间的冲突,开车的时候的跳跃键和射击时的肯定不一样
所以你可以在不同的模式下切换Action Maps从而做到满足两种游戏需求
在游戏中想要进行切换,可以通过脚本的
playerInput.SwitchCurrentActionMap("Menu");
Debug.Log(playerInput.currentActionMap);
在input system 中,Actions 连接了控制设备的物理输入和游戏内部的事件
Actions 是 Action maps的具体表现
对于Values和Pass Through的选择上,要看自己的设计需求
Values会优先考虑最大的输入值,而Pass Through会优先考虑最近的输入值,而不管它有多大。
input system 中的 Get Key 和 Get Key Down
如果使用老的input Manager, 判断一个键是按了一次还是按下了(例如移动),通过检查对按住按钮的Get key或对单次按下的Get key down来完成。
在 input system 中 Button Action Type 代表只触发一次,等价于 Get Key Down
Value Action Type 等价于 Get Key
GetKey 当通过名称指定的按键被用户按住时返回true
GetKeyDown 当用户按下指定名称的按键时的那一帧返回true。
GetKeyUp 在用户释放给定名字的按键的那一帧返回true。
在使用Value或者Pass Through Types时,你会看到一个额外的选项 Control Type
这允许您指定期望从输入中获得什么类型的输入,并影响哪些Bindings可用。
在完成上述的设置后,就可以添加Binding了
简单来说就是将物理输入与事件进行绑定
using UnityEngine;
using UnityEngine.InputSystem;
public class BasicRebinding : MonoBehaviour
{
public InputActionReference triggerAction;
void ChangeBinding()
{
InputBinding binding = triggerAction.action.bindings[0];
binding.overridePath = "/#(g)" ;
triggerAction.action.ApplyBindingOverride(0, binding);
}
}
在2D Axis Composite中,从两个axes中获取了值信息
然而这两个值会因为Composite mode 的不同而影响彼此
比如你推动上和右,那么得到的Vector2 就是 (1,1),一般用于两个不相关的控制
在人物移动时,往往会需要上下左右组合按,但如果同时按上右,这样得到的速度就会很大,与正常的逻辑相违背,这时就要使用Digital Normalized
默认使用,返回一个归一化的向量,这时如果按上右,那么得到的就是(0.7, 0.7)
限制大家应该也看出来了,那就是digital只能给两个方向的输入使用,如果是更多方向呢
比如遥杆控制、控制器的左右操纵杆,那么就需要新的方式
这种场景很常见,比如经常会有CTRL+Z等快捷组合键需要实现
在inputsystem中,可以创建Button with One Modifier Composite 或者 Button with Two Modifiers Composite
但是要注意的是,input system 并不能屏蔽其他的输入
举个例子,如果你绑定jump动作和B键,而另一个动作Dive绑定的是Left Trigger + B
那么两个动作都会触发,那么应该如何避免呢?
就是在触发时,做一个检测判断,看另一个按键有没有被触发,再决定下一个动作。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;
public class InputTester : MonoBehaviour
{
bool modifierPressed;
void OnModifier(InputValue value)
{
modifierPressed = value.isPressed;
}
void OnTrigger()
{
if(modifierPressed)
{
// Do one thing...
}
else
{
// Do a different thing...
}
}
}
当设置你的游戏控制器时,可以使用Interactions 和 Processors 改变输入的解释方法
比如,轻点和长按,就是对按钮行为的不同解释
用户在按压按钮时,可以有多种方法
可以快速的轻按,或者长时间的重按
也可以在按下和释放时,触发不同的Action
也可以快速双击
- Hold
之后按压一段时间后,才会触发事件
- Multi Tap
重复使用tap, 可以用这个方式实现双击操作
- Press
按压或者释放时触发事件
- Slow Tap
只是和Tap相比较而言具有更长的时间周期
- Tap
快速按压和释放
input system 为了方便用户使用,具有灵活性和可扩展性,提供了多种方式使用
其中最简单的使用方法是使用Player Input Component
切换 Action Maps
playerInput.SwitchCurrentActionMap("Menu");
使用Send Message时,每次的触发会盗用一个对应的函数
比如Input Action 名为 Jump,那么对应的函数即为 OnJump
就是在前面加个On-
可以通过Get 获取设置的对应类型的数据
using UnityEngine;
using UnityEngine.InputSystem;
public class MovePlayer : MonoBehaviour
{
public Vector2 moveVal;
public float moveSpeed;
void OnMove(InputValue value)
{
moveVal = value.Get<Vector2>();
}
void Update()
{
transform.Translate(new Vector3(moveVal.x, moveVal.y, 0) * moveSpeed * Time.deltaTime);
}
void OnFire(InputValue value)
{
float triggerVal = value.Get<float>();
}
bool modifierPressed;
void OnModifier(InputValue value)
{
modifierPressed = value.isPressed;
}
}
这种方式很适合对于单个游戏对象操控,对于多个对象可以使用
Broadcast Messages 与 send Message 很相似,只是broadcast 会通过object hierarchy 向下传递
public void Move(InputAction.CallbackContext value)
{
moveVal = value.ReadValue<Vector2>();
transform.Translate(new Vector3(moveVal.x, moveVal.y, 0));
}
using UnityEngine;
using UnityEngine.InputSystem;
public class CSharpEvent : MonoBehaviour
{
public PlayerInput playerInput;
void OnEnable()
{
playerInput.onActionTriggered += MyEventFunction;
}
void OnDisable()
{
playerInput.onActionTriggered -= MyEventFunction;
}
void MyEventFunction(InputAction.CallbackContext value)
{
Debug.Log(value.action.name + (" was triggered"));
}
}
鼠标主要有四个属性
Delta代表鼠标相对运动的坐标改变值,与上一帧的偏移
The current window-space motion delta of the pointer.
Postion代表鼠标当前绝对位置, 在空间中的坐标
The current pointer coordinates in window space.
Radius代表与屏幕的接触半径
Window-space radius of the pointer contact with the surface.
Usually, only touch input has radius detection
Sroll 代表滚轮
引用
context.phase中的内容,其中包含了按键信息的详细情况,
对应的case有
- Disabled 禁用
- Waiting 等待输入
- Started 开始输入
- Performed 完成输入事件(例如hold了足够的时间)
- Canceled 未完成输入事件(例如hold的时间不够)
InputAction包括
wasPressedThisFrame,
wasReleasedThisFrame,
isPressed
if (keyboard.wKey.wasPressedThisFrame)
Debug.Log("w键按下(一直按住w键的话,也只执行一次)");
if (keyboard.wKey.wasReleasedThisFrame)
Debug.Log("w键松开");
Debug.Log("是否按住w键:" + keyboard.wKey.isPressed);
在使用方法上,可以和老的输入方式类似,直接读取设备的状态
void Update()
{
Vector2 mousePosition = Mouse.current.position.ReadValue();
if(Keyboard.current.anyKey.wasPressedThisFrame)
{
Debug.Log("A key was pressed");
}
if (Gamepad.current.aButton.wasPressedThisFrame)
{
Debug.Log("A button was pressed");
}
}
Keyboard keyboard;
keyboard = InputSystem.GetDevice<Keyboard>();
if (keyboard.wKey.isPressed)
{
direction = new Vector2(0, 1);
Vector3 delta = new Vector3(direction.x, direction.y, 0) * Time.deltaTime * 5f;
transform.position = transform.position + delta;
}
// 按键按下
Keyboard.current.spaceKey.isPressed
// 按键按住
Keyboard.current.spaceKey.wasPressedThisFrame
在使用时,有时并不用各种Get,也可以直接使用,button就是bool
pitch是围绕X轴旋转,也叫做俯仰角
yaw是围绕Y轴旋转,也叫偏航角
roll是围绕Z轴旋转,也叫翻滚角
private void CameraRotation()
{
// if there is an input and camera position is not fixed
if (_input.look.sqrMagnitude >= _threshold && !LockCameraPosition)
{
//Don't multiply mouse input by Time.deltaTime;
float deltaTimeMultiplier = IsCurrentDeviceMouse ? 1.0f : Time.deltaTime;
_cinemachineTargetYaw += _input.look.x * deltaTimeMultiplier;
_cinemachineTargetPitch += _input.look.y * deltaTimeMultiplier;
}
// clamp our rotations so our values are limited 360 degrees
_cinemachineTargetYaw = ClampAngle(_cinemachineTargetYaw, float.MinValue, float.MaxValue);
_cinemachineTargetPitch = ClampAngle(_cinemachineTargetPitch, BottomClamp, TopClamp);
// Cinemachine will follow this target
CinemachineCameraTarget.transform.rotation = Quaternion.Euler(_cinemachineTargetPitch + CameraAngleOverride,
_cinemachineTargetYaw, 0.0f);
}
private void Move()
{
// set target speed based on move speed, sprint speed and if sprint is pressed
float targetSpeed = _input.sprint ? SprintSpeed : MoveSpeed;
// a simplistic acceleration and deceleration designed to be easy to remove, replace, or iterate upon
// note: Vector2's == operator uses approximation so is not floating point error prone, and is cheaper than magnitude
// if there is no input, set the target speed to 0
if (_input.move == Vector2.zero) targetSpeed = 0.0f;
// a reference to the players current horizontal velocity
float currentHorizontalSpeed = new Vector3(_controller.velocity.x, 0.0f, _controller.velocity.z).magnitude;
float speedOffset = 0.1f;
float inputMagnitude = _input.analogMovement ? _input.move.magnitude : 1f;
// accelerate or decelerate to target speed
if (currentHorizontalSpeed < targetSpeed - speedOffset ||
currentHorizontalSpeed > targetSpeed + speedOffset)
{
// creates curved result rather than a linear one giving a more organic speed change
// note T in Lerp is clamped, so we don't need to clamp our speed
_speed = Mathf.Lerp(currentHorizontalSpeed, targetSpeed * inputMagnitude,
Time.deltaTime * SpeedChangeRate);
// round speed to 3 decimal places
_speed = Mathf.Round(_speed * 1000f) / 1000f;
}
else
{
_speed = targetSpeed;
}
_animationBlend = Mathf.Lerp(_animationBlend, targetSpeed, Time.deltaTime * SpeedChangeRate);
if (_animationBlend < 0.01f) _animationBlend = 0f;
// normalise input direction
Vector3 inputDirection = new Vector3(_input.move.x, 0.0f, _input.move.y).normalized;
// note: Vector2's != operator uses approximation so is not floating point error prone, and is cheaper than magnitude
// if there is a move input rotate player when the player is moving
if (_input.move != Vector2.zero)
{
_targetRotation = Mathf.Atan2(inputDirection.x, inputDirection.z) * Mathf.Rad2Deg +
_mainCamera.transform.eulerAngles.y;
float rotation = Mathf.SmoothDampAngle(transform.eulerAngles.y, _targetRotation, ref _rotationVelocity,
RotationSmoothTime);
// rotate to face input direction relative to camera position
transform.rotation = Quaternion.Euler(0.0f, rotation, 0.0f);
}
Vector3 targetDirection = Quaternion.Euler(0.0f, _targetRotation, 0.0f) * Vector3.forward;
// move the player
_controller.Move(targetDirection.normalized * (_speed * Time.deltaTime) +
new Vector3(0.0f, _verticalVelocity, 0.0f) * Time.deltaTime);
// update animator if using character
if (_hasAnimator)
{
_animator.SetFloat(_animIDSpeed, _animationBlend);
_animator.SetFloat(_animIDMotionSpeed, inputMagnitude);
}
}
private void JumpAndGravity()
{
if (Grounded)
{
// reset the fall timeout timer
_fallTimeoutDelta = FallTimeout;
// update animator if using character
if (_hasAnimator)
{
_animator.SetBool(_animIDJump, false);
_animator.SetBool(_animIDFreeFall, false);
}
// stop our velocity dropping infinitely when grounded
if (_verticalVelocity < 0.0f)
{
_verticalVelocity = -2f;
}
// Jump
if (_input.jump && _jumpTimeoutDelta <= 0.0f)
{
// the square root of H * -2 * G = how much velocity needed to reach desired height
_verticalVelocity = Mathf.Sqrt(JumpHeight * -2f * Gravity);
// update animator if using character
if (_hasAnimator)
{
_animator.SetBool(_animIDJump, true);
}
}
// jump timeout
if (_jumpTimeoutDelta >= 0.0f)
{
_jumpTimeoutDelta -= Time.deltaTime;
}
}
else
{
// reset the jump timeout timer
_jumpTimeoutDelta = JumpTimeout;
// fall timeout
if (_fallTimeoutDelta >= 0.0f)
{
_fallTimeoutDelta -= Time.deltaTime;
}
else
{
// update animator if using character
if (_hasAnimator)
{
_animator.SetBool(_animIDFreeFall, true);
}
}
// if we are not grounded, do not jump
_input.jump = false;
}
// apply gravity over time if under terminal (multiply by delta time twice to linearly speed up over time)
if (_verticalVelocity < _terminalVelocity)
{
_verticalVelocity += Gravity * Time.deltaTime;
}
}
检查游戏手柄的a键是否被按压
Keyboard.current.space.wasPressedThisFrame;
Gamepad.current.aButton.wasPressedThisFrame
var allGamepads = Gamepad.all;
// Go through all devices and select gamepads.
InputSystem.devices.Select(x => x is Gamepad);
// Query everything that is using the gamepad template or based on that template.
InputSystem.GetControls("/" );
// Fetch all devices with "gamepad" in their names (not a good idea; no guarantee
// a gamepad is actually named that way).
InputSystem.GetControls("/gamepad*");
InputSystem.onDeviceChange +=
(device, change) =>
{
if (change == InputDeviceChange.Added)
/* New Device */;
else if (change == InputDeviceChange.Disconnected)
/* Device got unplugged */;
else if (change == InputDeviceChange.Connected)
/* Plugged back in */;
else if (change == InputDeviceChange.Removed)
/* Remove from input system entirely; by default, devices stay in the system once discovered */;
}
// Create action that binds to the primary action control on all devices.
var action = new InputAction(binding: "*/{primaryAction}");
// Have it run your code when action is triggered.
action.performed += _ => Fire();
// Start listening for control changes.
action.Enable();
第二种方式
首先
public class MyControllerComponent : MonoBehaviour
{
public InputAction fireAction;
public InputAction walkAction;
}
之后可以在编辑器的Inspector中进行绑定,之后响应绑定为
void Awake()
{
fireAction.performed += _ => Fire;
walkAction.performed += _ => Walk;
}
void OnEnable()
{
fireAction.Enable();
walkAction.Enable();
}
void OnDisable()
{
fireAction.Disable();
walkAction.Disable();
}
void Fire(InputAction action, InputControl control)
{
//...
}
void Walk(InputAction action, InputControl control)
{
//...
}
或者
void OnEnable()
{
fireAction.Enable();
walkAction.Enable();
}
void OnDisable()
{
fireAction.Disable();
walkAction.Disable();
}
void Update()
{
if (fireAction.hasBeenPerformedThisFrame)
Fire();
if (walkAction.hasBeenPerformedThisFrame)
Walk();
}
void Fire()
{
//...
}
void Walk(InputAction action, InputControl control)
{
//...
}
var action = new InputAction(binding: "*/{PrimaryAction}",
modifiers: "hold(duration=0.4)");
action.started += _ => ShowGunChargeUI();
action.performed += _ => FinishGunChargingAndHideChargeUI();
action.cancelled += _ => HideChargeUI()
var action = new InputAction();
action.AddBinding("//leftTrigger" )
.CombinedWith("//buttonSouth" , modifiers: "hold(duration=0.4)");
var myAction = new InputAction(binding: "/*/);
myAction.onPerformed += (action, control) => Debug.Log($"Button {control.name} pressed!");
myAction.Enable();
var gamepad = Gamepad.current;//手柄
var keyboard = Keyboard.current;//键盘
var mouse = Mouse.current;//鼠标
var pointer = Pointer.current;//指针
if (gamepad != null)
{
Debug.Log(gamepad.leftStick.ReadValue());//手柄遥感的偏移
if (gamepad.bButton.wasPressedThisFrame)
Debug.Log("按下B键");
}
if (keyboard != null)
{
//执行顺序 isPressed = false -> 按下:wasPressedThisFrame = true -> 中途:isPressed = true -> 松开:wasReleasedThisFrame = true -> isPressed = false
if (keyboard.wKey.wasPressedThisFrame)
Debug.Log("w键按下(一直按住w键的话,也只执行一次)");
if (keyboard.wKey.wasReleasedThisFrame)
Debug.Log("w键松开");
Debug.Log("是否按住w键:" + keyboard.wKey.isPressed);
}
if (mouse != null)
{
Debug.Log(mouse.scroll.ReadValue());//滚轮的滚动值,向前滚Y的值为正,向后滚为负
if (mouse.leftButton.wasPressedThisFrame)
Debug.Log("按鼠标左键");
if (mouse.rightButton.wasPressedThisFrame)
Debug.Log("按鼠标右键");
if (mouse.middleButton.wasPressedThisFrame)
Debug.Log("按滚轮键");
}
if (pointer != null)
{
Debug.Log(pointer.delta.ReadValue());//与上一帧的偏移
Debug.Log(pointer.position.ReadValue());//在空间中的坐标
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MyInputSmallTest : MonoBehaviour
{
// MyInputActions为自己的Action名字
public MyInputActions inputActions;
private void Awake()
{
inputActions = new MyInputActions();
inputActions.Enable();
// 键盘控制移动,且inputActions必须配合+=,-=使用
inputActions.Player.KBD_MOVE.performed += ctx =>
{
transform.Translate(ctx.ReadValue<Vector2>());
};
inputActions.Player.ACTION_DOWN.performed += ctx =>
{
Debug.Log("Action_Down" + UnityEngine.InputSystem.Mouse.current.position.ReadValue());
};
// 鼠标移动时即时反馈坐标
inputActions.Player.ACTION_MOVE.performed += ctx =>
{
Debug.Log("Action_Move" + ctx.ReadValue<Vector2>());
};
inputActions.Player.ACTION_UP.performed += ctx =>
{
Debug.Log("Action_Up" + UnityEngine.InputSystem.Mouse.current.position.ReadValue());
};
}
void Start(){}
void Update(){}
private void OnDestroy()
{
inputActions.Disable();
}
}
public InputManager inputActions;
private Vector2 direction;
private void Awake()
{
inputActions = new InputManager();
}
private void OnEnable()
{
inputActions.Enable();
inputActions.Player.Move.performed += ctx => Move(ctx);
inputActions.Player.Move.canceled += ctx => Move(ctx);
}
private void OnDisable()
{
inputActions.Player.Move.performed -= ctx => Move(ctx); ;
inputActions.Player.Move.canceled -= ctx => Move(ctx);
inputActions.Disable();
}
public void Move(InputAction.CallbackContext context)
{
var moveValue = context.ReadValue<Vector2>();
Debug.Log(moveValue);
direction = moveValue;
}
void Update()
{
if (direction != Vector2.zero)
{
Vector3 delta = new Vector3(direction.x, direction.y, 0) * Time.deltaTime * 5f;
transform.position = transform.position + delta;
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem.Interactions;
public class TictactoeControl : MonoBehaviour
{
private TictactoeAction InputActions;
private void Awake()
{
InputActions = new TictactoeAction();
InputActions.TestMap.Tap.started += ctx =>
{
Debug.Log("操作开始");
};
InputActions.TestMap.Tap.performed += ctx =>
{
if (ctx.interaction is MultiTapInteraction)
{
Debug.Log("执行双击逻辑");
}
else if (ctx.interaction is HoldInteraction)
{
Debug.Log("执行长按逻辑");
}
else
{
//列表中只有MultiTapInteraction和HoldInteraction对应的两种Interaction。
//故不会走到这个else里。
}
};
InputActions.TestMap.Tap.canceled += ctx =>
{
if (ctx.interaction is MultiTapInteraction)
{
Debug.Log("执行点击逻辑");
}
};
}
void Start()
{
}
void Update()
{
}
public void OnEnable()
{
InputActions.Enable();
}
public void OnDisable()
{
InputActions.Disable();
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.InputSystem.Interactions;
public class MouseInputPlayer : MonoBehaviour
{
private RaycastHit hit;
private bool hitBool; //是否点击到目标
private IMouseInputPlayer mouseClickInterface; //接口
private ObjectProperties mIPScriptName; // 物体属性脚本
private bool ifMouseClickInterface; // 是否存在
private bool ifObjectProperties; // 是否存在
public void OnLeftBottonClick(InputAction.CallbackContext context)
{
switch (context.phase)
{
case InputActionPhase.Started:
{
//Debug.Log("操作开始");
mouseClickInterface = null; //初始化接口
Ray ray = Camera.main.ScreenPointToRay(Mouse.current.position.ReadValue()); //射线
hitBool = Physics.Raycast(ray, out hit); //返回是否点击到目标
if (hitBool)
{
//Debug.Log("操作开始");
ifObjectProperties = hit.collider.gameObject.TryGetComponent(out objectProperties); // 获取物体属性脚本
ifMouseClickInterface = hit.collider.gameObject.TryGetComponent(out mouseClickInterface); //设置接口
}
}
break;
case InputActionPhase.Performed:
{
if (hitBool && ifObjectProperties)
{
if (context.interaction is MultiTapInteraction)
{
//Debug.Log("执行双击逻辑");
if (objectProperties.ifLeftMouseDblclick && ifMouseClickInterface)
{
mouseClickInterface.OnLeftMouseDblclick();
}
}
else if (context.interaction is HoldInteraction)
{
//Debug.Log("执行长按逻辑");
if (objectProperties.ifLeftMouseHold && ifMouseClickInterface)
{
mouseClickInterface.OnLeftMouseHold();
}
}
else
{
//列表中只有MultiTapInteraction和HoldInteraction对应的两种Interaction。
//故不会走到这个else里。
}
}
}
break;
case InputActionPhase.Canceled:
{
if (context.interaction is MultiTapInteraction)
{
//Debug.Log("执行点击逻辑");
if (hitBool && ifObjectProperties)
{
if (objectProperties.ifLeftMouseClick && ifMouseClickInterface)
{
mouseClickInterface.OnLeftMouseClick();
}
}
}
}
break;
}
}
}
后面看到好代码例子再加吧