Unity Input System 官方案例学习笔记

Unity Input System 官方案例学习笔记

01 前言

作者:hzb

开始时间:2023-12-9

最后更新时间:2023-12-11

资料来源:Unity Input System 官方案例

该文章将从Unity Input System的所给的官方案例中学习这个新的输入系统的使用,资料中所有中文由ChatGPT4提供翻译,同时部分补充即实例由GPT4提供,如有错误欢迎指出。

案例导入

  • Package Manager

image-20231211111345627

  • Project Settings

image-20231211111535392

02 案例: Simple Demo

这个示例展示了如何使用输入系统设置一个简单的角色控制器。由于有多种方式可以做到这一点,该示例展示了几种方法。每个演示都设置为一个单独的场景。所有场景中的基本功能都是相同的。你可以移动、四处看看,以及向场景中发射弹丸(彩色立方体)。在某些场景中,只支持游戏手柄,但更复杂的演示支持多种不同的输入同时使用。

(1) UsingState

案例分析
案例README

这个演示从最低级开始,展示了如何通过在MonoBehaviour.Update函数中直接轮询输入状态来连接输入。为了简单起见,它只处理游戏手柄,但对于其他类型的输入设备(例如使用Mouse.currentKeyboard.current)也以类似方式工作。

这里演示的关键API是Gamepad.currentInputControl.ReadValue

public class SimpleController_UsingState : MonoBehaviour
{
    //...

    public void Update()
    {
        var gamepad = Gamepad.current;
        if (gamepad == null)
            return;

        var move = Gamepad.leftStick.ReadValue();
        //...
    }
}

在这个演示中,主要关注点是如何直接从游戏手柄(或其他输入设备)获取实时输入数据,并基于这些数据来更新游戏中的角色或对象状态。示例可能包括如何读取摇杆位置、按钮按下状态等,来实现角色的移动、跳跃或其他动作。

关键步骤

关键特点和步骤可能包括:

  1. 获取当前游戏手柄
    • 使用 Gamepad.current 来获取当前连接的游戏手柄。
  2. 轮询按键状态
    • 检查特定按钮(如南方向按钮)在当前帧是否被按下或释放。
  3. 读取摇杆输入
    • 获取摇杆(如左摇杆 leftStick)的当前位置,这通常返回一个 Vector2 类型的值,表示摇杆的水平和垂直偏移。
  4. 应用输入到游戏逻辑
    • 根据获取到的输入值来控制角色的移动、转向或其他行为。
  5. 简单实现
    • 这种方法通常比较直接和简单,适合快速原型开发或小型项目。

SimpleDemo_UsingState是一个很好的起点,用于理解如何在 Unity 的新输入系统中处理原始输入数据。它特别适合那些需要对输入进行低级处理的应用场景。

代码注释
using UnityEngine.InputSystem;
using UnityEngine;

// 直接使用游戏手柄设备的状态。
public class SimpleController_UsingState : MonoBehaviour
{
    // 定义移动速度和旋转速度。
    public float moveSpeed;
    public float rotateSpeed;
    // 弹丸的预制体。
    public GameObject projectile;

    // 用于存储旋转状态和射击状态的私有变量。
    private Vector2 m_Rotation;
    private bool m_Firing;
    private float m_FireCooldown;

    public void Update()
    {
        // 获取当前连接的游戏手柄。
        var gamepad = Gamepad.current;
        if (gamepad == null)
            return; // 如果没有手柄连接,则退出。

        // 读取左右摇杆的值。
        var leftStick = gamepad.leftStick.ReadValue();
        var rightStick = gamepad.rightStick.ReadValue();

        // 根据右摇杆控制视角旋转,根据左摇杆控制移动。
        Look(rightStick);
        Move(leftStick);

        // 检查南方向按钮(例如A或X键)的按下和释放状态,控制射击。
        if (gamepad.buttonSouth.wasPressedThisFrame)
        {
            m_Firing = true;
            m_FireCooldown = 0;
        }
        else if (gamepad.buttonSouth.wasReleasedThisFrame)
        {
            m_Firing = false;
        }

        // 如果处于射击状态且射击冷却时间已到,进行射击。
        if (m_Firing && m_FireCooldown < Time.time)
        {
            Fire();
            m_FireCooldown = Time.time + 0.1f; // 设置下次射击的冷却时间。
        }
    }

    // 根据输入方向进行移动。
    private void Move(Vector2 direction)
    {
        if (direction.sqrMagnitude < 0.01)
            return; // 如果方向向量几乎为零,则不移动。
        var scaledMoveSpeed = moveSpeed * Time.deltaTime;
        // 将方向向量转换为世界坐标系下的移动。
        var move = Quaternion.Euler(0, transform.eulerAngles.y, 0) * new Vector3(direction.x, 0, direction.y);
        transform.position += move * scaledMoveSpeed; // 应用移动。
    }

    // 根据输入旋转角色。
    private void Look(Vector2 rotate)
    {
        if (rotate.sqrMagnitude < 0.01)
            return; // 如果旋转向量几乎为零,则不旋转。
        var scaledRotateSpeed = rotateSpeed * Time.deltaTime;
        // 计算新的旋转值,并应用到对象上。
        m_Rotation.y += rotate.x * scaledRotateSpeed;
        m_Rotation.x = Mathf.Clamp(m_Rotation.x - rotate.y * scaledRotateSpeed, -89, 89);
        transform.localEulerAngles = m_Rotation;
    }

    // 发射弹丸。
    private void Fire()
    {
        // 创建一个新的弹丸实例并设置其位置和方向。
        var transform = this.transform;
        var newProjectile = Instantiate(projectile);
        newProjectile.transform.position = transform.position + transform.forward * 0.6f;
        newProjectile.transform.rotation = transform.rotation;
        // 设置弹丸大小和物理属性。
        const int size = 1;
        newProjectile.transform.localScale *= size;
        newProjectile.GetComponent<Rigidbody>().mass = Mathf.Pow(size, 3);
        // 给弹丸一个前向力,使其发射。
        newProjectile.GetComponent<Rigidbody>().AddForce(transform.forward * 20f, ForceMode.Impulse);
        // 随机改变弹丸的颜色。
        newProjectile.GetComponent<MeshRenderer>().material.color =
            new Color(Random.value, Random.value, Random.value, 1.0f);
    }
}
案例总结

这种方法即每帧检查当前是否有游戏手柄接入,并获取这个手柄上各个按键的状态。这与旧的Unity Input Manager的使用方法相似,但有几个关键的区别和优势:

  1. 输入设备的直接访问:新的输入系统允许直接访问特定的输入设备(如 Gamepad)。这意味着您可以直接查询游戏手柄的状态,而不是通过一般的输入轴或按钮映射。
  2. 设备独立性:新的输入系统设计用来更好地处理多种输入设备,包括游戏手柄、键盘、鼠标等,每种设备都有其专有的类和方法。
  3. 更好的按键状态管理:新系统提供了更细致的按键状态查询,如 wasPressedThisFramewasReleasedThisFrame 等,这使得管理按键状态(例如检测单次按下或释放)更为简便。

总的来说,UsingState在设计上提供了更多的灵活性和控制力,特别是在处理多种输入设备和复杂的输入需求时。虽然其基本原理(即每帧检查输入状态)与旧的Input Manager相似,但新系统提供了更多的功能和更好的设备管理。

案例补充
Gamepad

在 Unity 的新输入系统中使用 Gamepad 类时,通常会用到以下几种状态和功能:

  1. 按键状态
    • wasPressedThisFrame: 在当前帧中,该按键是否被首次按下。
    • wasReleasedThisFrame: 在当前帧中,该按键是否被释放。
    • isPressed: 该按键当前是否被持续按下。
  2. 摇杆和触摸板
    • leftStick, rightStick: 分别代表左右摇杆的当前位置(返回 Vector2)。
    • dpad: 方向键的当前位置(也是 Vector2)。
  3. 触发器
    • leftTrigger, rightTrigger: 游戏手柄上左右触发器的当前值(通常是0到1的浮点数)。
  4. 按钮
    • buttonSouth(通常是A或X键)、buttonNorth(通常是Y或△键)、buttonEast(通常是B或○键)、buttonWest(通常是X或□键)。
    • startButton, selectButton: 分别代表开始和选择按钮。
  5. 震动功能
    • SetMotorSpeeds(float lowFrequency, float highFrequency): 设置游戏手柄的震动强度。
  6. 设备状态
    • Gamepad.current: 获取当前连接的游戏手柄。
    • Gamepad.all: 获取所有连接的游戏手柄列表。

使用这些状态和功能,可以检测游戏手柄上的按键、摇杆、触发器的当前状态,以及设置震动等。这对于创建游戏中的角色控制、菜单导航、交互响应等功能非常有用。通过这些API,可以为玩家提供丰富而直观的游戏操作体验。

KeyBoard

在 Unity 的新输入系统中,针对键盘的常用状态和功能包括以下几个方面:

  1. 按键状态
    • wasPressedThisFrame: 在当前帧中,指定的键是否被首次按下。
    • wasReleasedThisFrame: 在当前帧中,指定的键是否被释放。
    • isPressed: 指定的键当前是否被持续按下。
  2. 特定按键的访问
    • 可以通过 Keyboard.current.<keyName>.ReadValue()Keyboard.current.<keyName>.isPressed 等方式访问特定键的状态,其中 <keyName> 是键的名称,例如 spaceKeyenterKeyleftShiftKey 等。
  3. 所有按键的状态
    • Keyboard.current.allKeys: 获取一个包含所有按键的数组,可以用来遍历或查询特定键的状态。
  4. 键盘的整体状态
    • Keyboard.current: 获取当前连接的键盘。
  5. 文本输入
    • onTextInput: 事件,可以用来接收文本输入,特别是对于需要文本输入(如聊天或表单填写)的应用。
  6. 修饰键状态
    • shiftKey, ctrlKey, altKey, metaKey(Windows上的Windows键,Mac上的Command键)等,用于检测这些修饰键是否被按下。
  7. 键盘布局
    • Keyboard.current.keyboardLayout: 获取当前键盘布局的名称。

通过这些API,可以精确地控制键盘输入,响应玩家的按键操作,实现角色控制、UI交互、文本输入等功能。这对于确保玩家能够通过键盘与游戏或应用程序有效交互非常重要。

键盘的每个键都有一个对应的名称,用于标识和访问该键的状态。以下是一些常用键的名称列表:

  1. 字母键:
    • aKey, bKey, cKey, dKey, eKey, fKey, gKey, hKey, iKey, jKey, kKey, lKey, mKey, nKey, oKey, pKey, qKey, rKey, sKey, tKey, uKey, vKey, wKey, xKey, yKey, zKey
  2. 数字键:
    • digit1Key, digit2Key, digit3Key, digit4Key, digit5Key, digit6Key, digit7Key, digit8Key, digit9Key, digit0Key
  3. 功能键:
    • f1Key, f2Key, f3Key, f4Key, f5Key, f6Key, f7Key, f8Key, f9Key, f10Key, f11Key, f12Key
  4. 修饰键:
    • leftShiftKey, rightShiftKey, leftCtrlKey, rightCtrlKey, leftAltKey, rightAltKey, leftMetaKey, rightMetaKeyMeta键通常是 Windows 键或 Mac 上的 Command 键)
  5. 空格和回车键:
    • spaceKey, enterKey
  6. 方向键:
    • upArrowKey, downArrowKey, leftArrowKey, rightArrowKey
  7. 其他常用键:
    • escapeKey, tabKey, capsLockKey, backspaceKey, deleteKey, homeKey, endKey, pageUpKey, pageDownKey, insertKey
  8. 数字小键盘键:
    • numpadEnterKey, numpadDivideKey, numpadMultiplyKey, numpadPlusKey, numpadMinusKey, numpadPeriodKey, numpadEqualsKey, numpad0Key, numpad1Key, numpad2Key, numpad3Key, numpad4Key, numpad5Key, numpad6Key, numpad7Key, numpad8Key, numpad9Key

要访问这些键的状态,你可以使用如下方式:

var keyboard = Keyboard.current;
if (keyboard.aKey.isPressed)
{
    // Do something when the 'A' key is pressed
}

请注意,上述列表并不完整,还有更多特殊用途的键(如多媒体控制键、功能区键等)可在 Unity 的文档中找到。每个键都对应于 Keyboard 类中的一个属性。

Mouse

在 Unity 的新输入系统中,鼠标的操作主要涉及以下几个方面:

  1. 指针移动
    • Mouse.current.delta: 鼠标指针自上一帧以来的移动量(Vector2 类型)。
  2. 按钮点击
    • Mouse.current.leftButton, Mouse.current.rightButton, Mouse.current.middleButton: 分别表示鼠标的左键、右键和中键。每个按钮都有 isPressed, wasPressedThisFrame, wasReleasedThisFrame 等状态。
  3. 滚轮滚动
    • Mouse.current.scroll: 鼠标滚轮的滚动量(Vector2 类型),其中 y 分量通常表示垂直滚动,x 分量表示水平滚动(如果支持)。
  4. 指针位置
    • Mouse.current.position: 鼠标指针在屏幕坐标中的当前位置(Vector2 类型)。
  5. 其他特殊按钮(如果鼠标有额外的按钮):
    • Mouse.current.forwardButton, Mouse.current.backButton 等。

使用这些属性和方法,你可以在 Unity 中实现对鼠标输入的精确控制。例如,你可以检测鼠标按钮的点击,处理拖拽操作,读取指针移动以实现相机控制,或者响应滚轮的滚动。

下面是一个简单的使用鼠标输入的例子:

void Update()
{
    var mouse = Mouse.current;

    if (mouse == null)
        return;

    // 检测鼠标左键是否被按下
    if (mouse.leftButton.wasPressedThisFrame)
    {
        Debug.Log("鼠标左键被按下");
    }

    // 获取鼠标的移动量
    Vector2 mouseMovement = mouse.delta.ReadValue();
}

在使用鼠标的过程中,请确保在检查鼠标状态前检查 Mouse.current 是否为 null,以防止在没有鼠标连接的情况下出现错误。

(2) UsingActions

案例分析
案例README

这个演示提高了一个层次,将输入转移到“输入动作”上。这些是允许您间接绑定到输入源的输入抽象。

在这个场景中,动作直接嵌入到角色控制器组件中。这允许直接在检查器中设置动作的绑定。要查看动作及其绑定,请在层级视图中选择Player对象并查看检查器中的SimpleController_UsingActions组件。

这里演示的关键API是InputAction及其Enable/Disable方法和ReadValue方法。

public class SimpleController_UsingActions : MonoBehaviour
{
    public InputAction moveAction;
    //...

    public void OnEnable()
    {
        moveAction.Enable();
        //...
    }

    public void OnDisable()
    {
        moveAction.Disable();
        //...
    }

    public void Update()
    {
        var move = moveAction.ReadValue<Vector2>();
        //...
    }
}

image-20231209200741109

具体步骤
  • 定义一个InputAction

    public InputAction moveAction;
    
  • 点击齿轮设置Action具体配置(下文具体介绍)

    image-20231210152223661

  • 点击加号添加输入监听绑定,

    • 一个按键绑定的Binding
    • 分离上下左右复合成一个vector2,如wasd
    • 自定义Composite
    • 一个前置按键+另一个按键的绑定,如Ctrl+A
    • 两个前置按键+另一个按键的绑定,如Ctrl+Shift+A

    image-20231210152328630

  • 启用Action

    	public void OnEnable()
      	{
            moveAction.Enable();
            //...
        }
    
        public void OnDisable()
        {
            moveAction.Disable();
            //...
        }
    
  • 通过给事件添加回调函数或者读取改事件的值完成逻辑设计,比如按键按下时触发事件,或者读取当前WASD输入得到的方向向量

    jumpAction.started += context => OnJumpStarted(context);
    jumpAction.performed += context => OnJumpPerformed(context);
    jumpAction.canceled += context => OnJumpCanceled(context);
    
    //或者
    var move = moveAction.ReadValue<Vector2>();
    Move(move);
    
    private void Move(Vector2 direction)
    {
        if (direction.sqrMagnitude < 0.01)
            return;
        var scaledMoveSpeed = moveSpeed * Time.deltaTime;
        // For simplicity's sake, we just keep movement in a single plane here. Rotate
        // direction according to world Y rotation of player.
        var move = Quaternion.Euler(0, transform.eulerAngles.y, 0) * new Vector3(direction.x, 0, direction.y);
        transform.position += move * scaledMoveSpeed;
    }
    
代码注释
using System.Collections;
using UnityEngine.InputSystem;
using UnityEngine;
using UnityEngine.InputSystem.Interactions;

// 这个类用于演示如何使用Unity的新输入系统进行基本的角色控制。
// 它包含移动、旋转和射击功能。
public class SimpleController_UsingActions : MonoBehaviour
{
    // 公共变量,可以在Unity编辑器中设置。它们控制角色的移动速度、旋转速度和射击爆发速度。
    public float moveSpeed;
    public float rotateSpeed;
    public float burstSpeed;
    public GameObject projectile; // 射击时将发射的弹丸预制体。

    // InputAction变量,代表玩家的不同输入动作:移动、观察和射击。
    public InputAction moveAction;
    public InputAction lookAction;
    public InputAction fireAction;

    // 一个私有变量,用于追踪玩家是否正在充能射击。
    private bool m_Charging;

    // 用于存储和更新角色的旋转值。
    private Vector2 m_Rotation;

    // 在对象被创建时调用的Awake方法。
    public void Awake()
    {
        // 为移动动作绑定三个回调函数:开始、执行和取消。
        // 这些回调将在相应的动作发生时被调用,并输出日志信息。
        moveAction.started +=
            ctx =>
        {
            Debug.Log("moveAction started");
        };
        moveAction.performed +=
            ctx =>
        {
            Debug.Log("moveAction performed");
        };
        moveAction.canceled +=
            ctx =>
        {
            Debug.Log("moveAction canceled");
        };

        // 为射击动作设置回调函数,这些回调将根据玩家输入的不同(如普通点击或长按)执行不同的逻辑。
        fireAction.started +=
            ctx =>
        {
            if (ctx.interaction is SlowTapInteraction)
                m_Charging = true; // 如果是慢速点击交互,开始充能。
        };

        fireAction.performed +=
            ctx =>
        {
            if (ctx.interaction is TapInteraction)
            {
                Fire(); // 如果是普通点击,执行普通射击。
            }
            else if (ctx.interaction is SlowTapInteraction)
            {
                StartCoroutine(BurstFire((int)(ctx.duration * burstSpeed))); // 如果是慢速点击,执行爆发射击。
            }
            m_Charging = false; // 射击完成后停止充能。
        };

        fireAction.canceled +=
            ctx =>
        {
            m_Charging = false; // 如果射击动作被取消,停止充能。
        };
    }

    // 当对象启用时调用,用于启用所有的InputAction。
    public void OnEnable()
    {
        moveAction.Enable();
        lookAction.Enable();
        fireAction.Enable();
    }

    // 当对象禁用时调用,用于禁用所有的InputAction。
    public void OnDisable()
    {
        moveAction.Disable();
        lookAction.Disable();
        fireAction.Disable();
    }

    // 在屏幕上绘制用户界面元素,例如充能状态。
    public void OnGUI()
    {
        if (m_Charging)
            GUI.Label(new Rect(100, 100, 200, 100), "Charging..."); // 如果正在充能,显示充能信息。
    }

    // 每帧调用一次的Update方法,用于读取并处理玩家的移动和旋转输入。
    public void Update()
    {
        var look = lookAction.ReadValue<Vector2>(); // 读取观察动作的值。
        var move = moveAction.ReadValue<Vector2>(); // 读取移动动作的值。

        // 先处理旋转,然后处理移动。这样做可以防止移动方向与旋转方向不同步。
        Look(look);
        Move(move);
    }

    // 处理角色的移动逻辑。
    private void Move(Vector2 direction)
    {
        if (direction.sqrMagnitude < 0.01)
            return; // 如果移动方向的幅度非常小,则不执行移动。

        // 将移动速度调整为每帧的移动距离。
        var scaledMoveSpeed = moveSpeed * Time.deltaTime; 
        var move = Quaternion.Euler(0, transform.eulerAngles.y, 0) * new Vector3(direction.x, 0, direction.y);
        // 根据输入方向和调整后的速度移动角色。
        transform.position += move * scaledMoveSpeed; 
    }

    // 处理角色的旋转逻辑。
    private void Look(Vector2 rotate)
    {
        if (rotate.sqrMagnitude < 0.01)
            return; // 如果旋转方向的幅度非常小,则不执行旋转。
        
		// 将旋转速度调整为每帧的旋转角度。
        var scaledRotateSpeed = rotateSpeed * Time.deltaTime; 
        // 根据输入更新角色的水平旋转。
        m_Rotation.y += rotate.x * scaledRotateSpeed; 
        // 根据输入更新角色的垂直旋转,并限制在-89至89度之间。
        m_Rotation.x = Mathf.Clamp(m_Rotation.x - rotate.y * scaledRotateSpeed, -89, 89); 
        transform.localEulerAngles = m_Rotation; // 应用新的旋转角度。
    }

    // 协程,用于实现爆发射击。
    private IEnumerator BurstFire(int burstAmount)
    {
        for (var i = 0; i < burstAmount; ++i) // 连续发射指定数量的弹药。
        {
            Fire();
            yield return new WaitForSeconds(0.1f); // 在每次射击之间等待0.1秒。
        }
    }

    // 创建并发射一个新的弹药实例。
    private void Fire()
    {
        var transform = this.transform;
        // 实例化弹药预制体。
        var newProjectile = Instantiate(projectile); 
        // 设置弹药的初始位置。
        newProjectile.transform.position = transform.position + transform.forward * 0.6f; 
        // 设置弹药的初始旋转
        newProjectile.transform.rotation = transform.rotation; 。
        var size = 1; // 设置弹药的大小。
        // 应用大小调整。
        newProjectile.transform.localScale *= size;
        // 根据大小调整弹药的质量。
        newProjectile.GetComponent<Rigidbody>().mass = Mathf.Pow(size, 3); 
        // 给弹药一个向前的冲力。
        newProjectile.GetComponent<Rigidbody>().AddForce(transform.forward * 20f, ForceMode.Impulse); 
        // 随机设置弹药的颜色。
        newProjectile.GetComponent<MeshRenderer>().material.color =
            new Color(Random.value, Random.value, Random.value, 1.0f); 
    }
}

案例补充:InputAction
概念介绍

image-20231209201941606

在 Unity 的新输入系统中,InputAction 是一个非常核心的概念。它代表了一种从物理输入设备(如键盘、鼠标、游戏手柄)到游戏逻辑之间的抽象映射。使用 InputAction,开发者可以定义如何将输入设备上的特定操作(如按键、鼠标移动、手柄摇杆)转化为游戏中的具体行为。

主要特点:

  1. 抽象与灵活性
    • InputAction 允许将输入从特定的按键或设备控件中抽象出来。这意味着你可以定义“跳跃”或“射击”这样的动作,而不用关心玩家是通过哪个按键或按钮触发的。
  2. 多设备支持
    • 一个 InputAction 可以同时从多个设备(如键盘和游戏手柄)接收输入,使得处理多平台输入更加方便。
  3. 可重映射
    • 玩家可以在游戏运行时重新映射控制,而不需要改变游戏逻辑代码。
  4. 事件驱动
    • InputAction 支持基于事件的输入处理,例如“动作开始”、“动作进行”和“动作结束”等。
具体步骤
  • 定义一个InputAction

    public InputAction moveAction;
    
  • 点击齿轮设置Action具体配置(下文具体介绍)

    image-20231210152223661

  • 点击加号添加输入监听绑定,

    • 一个按键绑定的Binding
    • 分离上下左右复合成一个vector2,如wasd
    • 自定义Composite
    • 一个前置按键+另一个按键的绑定,如Ctrl+A
    • 两个前置按键+另一个按键的绑定,如Ctrl+Shift+A

    image-20231210152328630

  • 启用Action

    	public void OnEnable()
      	{
            moveAction.Enable();
            //...
        }
    
        public void OnDisable()
        {
            moveAction.Disable();
            //...
        }
    
  • 通过给事件添加回调函数或者读取改事件的值完成逻辑设计,比如按键按下时触发事件,或者读取当前WASD输入得到的方向向量

    jumpAction.started += context => OnJumpStarted(context);
    jumpAction.performed += context => OnJumpPerformed(context);
    jumpAction.canceled += context => OnJumpCanceled(context);
    
    //或者
    var move = moveAction.ReadValue<Vector2>();
    Move(move);
    
    private void Move(Vector2 direction)
    {
        if (direction.sqrMagnitude < 0.01)
            return;
        var scaledMoveSpeed = moveSpeed * Time.deltaTime;
        // For simplicity's sake, we just keep movement in a single plane here. Rotate
        // direction according to world Y rotation of player.
        var move = Quaternion.Euler(0, transform.eulerAngles.y, 0) * new Vector3(direction.x, 0, direction.y);
        transform.position += move * scaledMoveSpeed;
    }
    

InputAction将物理输入与游戏逻辑相分离,我们需要一个Vector2来表示移动的方向,这个方向从物理输入得到,但是我们用InputAction将其分离,定义一个MoveAction来获取输入,这个MoveAction触发时会返回一个Vector2,至于如何触发,由外部绑定设置,如上面的简单案例,逻辑实现时,只需要知道MoveAction会返回一个向量,至于如何触发,则在外部绑定至手柄左摇杆和WASD

生命周期事件与回调函数
Action事件
jumpAction.started += context => OnJumpStarted(context);
jumpAction.performed += context => OnJumpPerformed(context);
jumpAction.canceled += context => OnJumpCanceled(context);
  • 每个Action有三个事件,分别为StartedPerformedCanceled
    • started 事件
      • InputAction 首次接收到输入并开始处理时触发。
      • 对于 Value 类型的动作,这通常是当控件的值开始变化时,例如摇杆开始移动或按键开始按下。
    • performed 事件
      • InputAction 接收到并处理输入值时持续触发。
      • 对于 Value 类型的动作,只要输入值持续变化,performed 事件就会在每个 Update 循环中触发。例如,如果你持续移动摇杆,performed 会持续被调用,提供摇杆的最新位置。
    • canceled 事件
      • InputAction 的输入停止或不再满足触发条件时触发。
      • 对于 Value 类型的动作,这通常是当控件的值停止变化时,例如摇杆回到中心位置或按键被释放。
CallbackContext
  • 事件触发时,会传递一个CallbackContext给回调函数,在 Unity 的新输入系统中,InputAction.CallbackContext(通常在回调中简称为 context)是一个非常重要的对象,提供了丰富的信息和功能,使开发者能够精确地处理和响应输入事件。以下是 context 的主要作用和提供的功能:

    1. 获取输入值
      • context.ReadValue<T>() 允许你根据动作的绑定类型读取当前的输入值。例如,对于绑定到摇杆的动作,你可以使用 context.ReadValue<Vector2>() 来获取摇杆的当前位置。
    2. 访问触发动作的控件
      • context.control 提供了对触发当前动作的具体输入控件的引用。这对于了解哪个具体的按键或摇杆触发了动作非常有用。
    3. 检查输入事件的时间
      • context.time 返回输入事件发生的时间(自游戏开始以来的秒数)。这对于实现基于时间的逻辑(如双击检测)非常有用。
    4. 确定动作的阶段
      • context.phase 表明动作当前的生命周期阶段,例如 StartedPerformedCanceled。这有助于区分输入的不同阶段,比如分别处理按键按下和释放。
    5. 访问相关的交互
      • 如果动作配置了特定的交互(如长按),context.interaction 提供了对当前处理输入的交互对象的引用。这使你能够获取交互的更多详细信息,如长按的持续时间。
    6. 访问触发回调的动作
      • context.action 提供了对触发回调的 InputAction 的引用。这允许你在回调中访问动作的属性,如名称或绑定。
    7. 处理多个绑定和控件
      • 在配置了多个绑定或控件的动作中,context 让你能够确定是哪个具体的绑定或控件触发了动作。
  • 使用 context,你可以根据输入事件的具体情况实现复杂和精细的输入处理逻辑。它提供了必要的信息,使得开发者能够根据不同的输入类型、来源和状态灵活地执行不同的行为。这在实现复杂的游戏控制、响应式用户界面或其他高度交互式应用程序中尤其重要。

image-20231210142258364

ActionType
基本概念
  • value:

    • 这种类型用于持续追踪一个值的变化,通常用于模拟输入,如摇杆的移动或鼠标的位置。
    • 输入数据是连续变化的,比如一个轴的位置或角度,就应该使用这种类型。
    • 这种类型的动作通常会在每次 Update 循环中生成值
    • 如果是value类型的Action,则还会有一个**control type**,用于指定这个Action绑定的按键数值类型,如遥感则是vector2等。
      • 确保兼容性
        • 指定 ControlType 可以确保只有兼容的输入控件能够被绑定到特定的 InputAction。例如,如果你的动作是为了处理模拟摇杆的移动,你可能会指定 ControlTypeVector2,这样就只有能够提供 Vector2 类型数据的控件(如摇杆)可以被绑定。
      • 提高输入处理的准确性
        • 通过限制可以绑定到动作的控件类型,你可以避免不正确的输入数据类型被送入动作的处理逻辑中,从而提高整个输入处理流程的准确性和可靠性。
      • 自动化的绑定过滤
        • 当设置了 ControlType,输入系统会自动过滤出只有符合指定类型的输入控件才能与该动作绑定。这在编辑器中创建绑定或在动态创建绑定时特别有用。
  • buttom:

    • 这种类型专为按钮或开关设计,它会产生单一的按下和释放事件。
    • 不是持续追踪值的变化,而是关注离散的事件,比如按钮是否被按下或释放。
    • 当你只关心一个按钮的按下和释放时(如跳跃或射击按钮),就应该使用这种类型。
    • 如果是Button类型的Action,则会有一个**Initial State Check的选项,这个选项用于在动作启用时检查按钮的初始状态**。具体来说,这意味着当动作被启用(比如在游戏开始或场景加载时)时,输入系统会立即检查与该动作绑定的按钮当前是否已经被按下。如果是,动作会立即触发 performed 事件,就好像按钮在那一刻被按下一样。
      • 处理游戏暂停或场景切换
        • 在游戏暂停然后恢复,或者在场景之间切换时,玩家可能已经按下了某个按钮。使用 InitialStateCheck 可以确保这些已经被按下的按钮不会被忽略。
      • 避免输入丢失
        • 在某些情况下,如果按钮在动作启用之前就被按下,不使用 InitialStateCheck 可能会导致动作错过这个按键事件。启用这个选项可以确保即使动作被稍后启用,之前的按键动作也会被正确处理。
      • 即时响应
        • 对于需要即时响应玩家输入的游戏或应用程序,使用 InitialStateCheck 可以确保在动作启用时立即检测并响应当前的按钮状态。
  • Pass Through

    • 这种类型允许输入信号“穿透”而不去尝试确定其当前状态。它不会尝试追踪动作的开始或结束,而只是在每次输入发生时提供数据。

    • 适用于你不需要确定动作状态,只需要知道何时接收到输入信号的情况。

    • 例如,你可能使用这种类型来收集原始输入数据用于自定义处理

  • 根据你的游戏或应用程序的具体需求,选择合适的 InputAction 类型可以帮助你更有效地处理玩家输入。例如,对于控制飞机的倾斜角度,你可能会选择 Value 类型;对于射击按钮,你可能会选择 Button 类型;而如果你正在创建一个自定义的输入记录器,可能会选择 Pass Through 类型。

使用 Value 类型的Action

在 Unity 的新输入系统中,当 InputAction 的类型(type)被设置为 Value 时,这意味着该动作用于持续监测和报告输入值的变化。Value 类型的动作非常适用于那些产生连续数据的输入,比如摇杆的移动、鼠标的位置或触摸屏的触摸点。

  1. 创建和配置: 在代码中,你可以创建一个 Value 类型的 InputAction 并为其绑定相应的输入源。例如,绑定到游戏手柄的摇杆或键盘的特定按键。

    public InputAction moveAction;
    
  2. 读取输入值: 在 InputAction 被启用后,你可以在更新循环中读取输入值。对于 Value 类型的动作,ReadValue<T>() 方法返回的是输入的当前值。

    Vector2 moveInput = moveAction.ReadValue<Vector2>();
    

    在这个例子中,我们假设 moveAction 绑定到了游戏手柄的左摇杆,因此 ReadValue<Vector2>() 会返回一个 Vector2,表示摇杆的位置。

  3. 注册事件回调: 你还可以为 Value 类型的动作注册事件回调,例如 performedcanceled,以便在输入值达到某个条件时执行特定的代码。它们的触发时机如下:

    1. started 事件
      • InputAction 首次接收到输入并开始处理时触发。
      • 对于 Value 类型的动作,这通常是当控件的值开始变化时,例如摇杆开始移动或按键开始按下。
    2. performed 事件
      • InputAction 接收到并处理输入值时持续触发。
      • 对于 Value 类型的动作,只要输入值持续变化,performed 事件就会在每个 Update 循环中触发。例如,如果你持续移动摇杆,performed 会持续被调用,提供摇杆的最新位置。
    3. canceled 事件
      • InputAction 的输入停止或不再满足触发条件时触发。
      • 对于 Value 类型的动作,这通常是当控件的值停止变化时,例如摇杆回到中心位置或按键被释放。
  4. 启用和禁用动作: 在适当的时候(如在 MonoBehaviourOnEnableOnDisable 方法中),你需要启用和禁用 InputAction

    private void OnEnable()
    {
        moveAction.Enable();
    }
    
    private void OnDisable()
    {
        moveAction.Disable();
    }
    
  5. 处理输入数据: 根据读取到的输入值,你可以实现具体的游戏逻辑,比如根据摇杆的位置移动角色或根据滑动的方向滚动屏幕。

使用 Button 类型的动作

在 Unity 的新输入系统中,当 InputAction 的类型(type)被设置为 Button 时,这表示该动作被设计来处理类似按钮的二元输入(即按下或释放)。Button 类型的动作非常适用于那些产生离散事件的输入,比如普通按钮、触摸屏的触摸或鼠标点击。

  1. 创建和配置: 在代码中,你可以创建一个 Button 类型的 InputAction 并为其绑定相应的输入源。例如,绑定到游戏手柄的某个按钮或键盘的特定按键。

    public InputAction jumpAction;
    
  2. 注册事件回调: 为 Button 类型的动作注册 startedperformedcanceled 事件回调,以便在按键被按下、持续按压或释放时执行特定的代码。

    jumpAction.started += context => OnJumpStarted(context);
    jumpAction.performed += context => OnJumpPerformed(context);
    jumpAction.canceled += context => OnJumpCanceled(context);
    

    在这些回调中,context 提供了关于输入事件的详细信息。

  3. 启用和禁用动作: 在适当的时候(如在 MonoBehaviourOnEnableOnDisable 方法中),需要启用和禁用 InputAction

    private void OnEnable()
    {
        jumpAction.Enable();
    }
    
    private void OnDisable()
    {
        jumpAction.Disable();
    }
    
  4. 处理输入事件: 根据输入事件的回调,实现具体的游戏逻辑,比如玩家按下跳跃键时使角色跳跃。

Interactions

image-20231211101401329

基本概念

在 Unity 的新输入系统中,InputActionInteractions 是一种强大的机制,它允许开发者定义如何解释和处理特定的输入模式或用户行为。简而言之,交互(Interactions)能够改变输入动作的触发方式,使其能够适应不同的输入模式,如长按、双击、序列输入等。

  • 交互的作用和常见类型

    1. 修改输入的触发行为

      • 交互可以改变输入动作的触发行为。例如,而不是在按键被按下时立即触发,一个配置了长按交互的动作会等到按键被持续按住一定时间后才触发。
      1. 实现复杂的输入逻辑
        • 使用交互,你可以实现更复杂的输入逻辑,如区分单击和双击,或者在用户完成一系列操作后触发动作。
    2. 提供更丰富的用户体验

      • 通过交互,你可以为用户提供更丰富的交互体验,如长按按钮来激活特殊能力,或者通过快速连续点击来加速游戏中的某个过程。
  • 常见的交互类型

    • Press:普通的按压交互,可以进一步配置为检测按下或释放。
    • SlowTap:长按交互,只有当按键被按住超过指定的时间时才触发。
    • Tap:轻敲交互,用于检测快速的按下和释放动作。
    • MultiTap(双击或多击):多次轻敲交互,用于检测快速连续的多次点击。
    • Hold:持有交互,用于检测按键被持续按住的情况。
  • 输入会按Interaction顺序进入不同的阶段,注意顺序的安排。

Press

image-20231210135054870

  • 在Unity的Input System中,Press 交互中的 Trigger Behavior 是一个关键配置,它定义了按键按下(Press)的行为何时触发一个事件。Trigger Behavior 有几种不同的选项,每个选项都会以不同的方式解释按键的按下和释放动作。以下是常见的 Trigger Behavior 选项及其用途:
    1. Press Only: 此选项意味着事件将仅在按键首次被按下时触发。这对于那些只需要在按键开始时响应一次的动作来说非常适用,例如射击或跳跃。
    2. Release Only: 与 Press Only 相反,此选项使得事件仅在按键被释放时触发。这在需要在结束输入时触发动作的场景下很有用,例如停止移动或结束攻击动作。
    3. Press and Release: 此选项将使事件在按键被按下时触发,同时在按键被释放时再次触发。这对于需要在按键按下和释放时都响应的动作非常有用,例如切换武器模式或开关灯。
  • 通常情况下不使用该功能,直接在使用无Interactions的就行,然后在回调函数中设置具体在按下还是释放时调用。
Tap

image-20231210135529037

  • 在 Unity 的新输入系统中,Tap 是一种交互类型,用于检测快速的点击或轻敲动作。当你为 InputAction 添加 Tap 交互时,它会配置动作以响应快速的按下和释放序列。Tap 交互适用于像是点击按钮或轻触屏幕这样的快速交互场景。此外,Tap 交互还有一些参数,如 MaxTapDurationPressPoint,这些参数可以进一步定制轻敲动作的行为。

    1. MaxTapDuration
      • 这个参数定义了按键按下后允许的最长持续时间,以使其仍然被视为一次“轻敲”。
      • 如果按键被按住的时间超过 MaxTapDuration 指定的值,则不会被视为轻敲。
      • 这个参数有助于区分快速点击和较长时间的按压。
    2. PressPoint
      • PressPoint 定义了触发轻敲动作的按键压力阈值。这对于模拟输入(如压力感应按键)特别有用。
      • 只有当按键的压力值超过 PressPoint 指定的阈值时,按键才被视为被“按下”,并开始计算轻敲的持续时间。
  • 使用案例如下:

    debugAction.performed += ctx =>
    {
    	if (ctx.interaction is TapInteraction)
    	{
    		Debug.Log("TapInteraction");
    	}
    };
    
  • 当按键持续事件超过0.2秒则不会视作Tap,即不会触发回调函数

SlowTap

image-20231210150623342

  • InputActionInteractions中,“slowTap” 是一种交互方式,它用于捕获用户在按下某个按钮后,在相对较长的时间内释放按钮的情况。具体来说,slowTap交互适用于以下情况:

    1. 长按操作:当用户按住一个按钮一段时间,然后释放它,以触发某个操作。这通常用于实现长按效果,比如在游戏中长按按钮来执行某个特殊动作。
    2. 按钮释放延迟:当用户按下按钮后,不立即释放,而是等待一段时间后释放按钮。这可以用于实现需要用户在一定时间内确认的操作,以避免误操作。
  • SlowTap具体有MinTapDurationPressPoint,两个参数来决定当前输入是否触发SlowTap

    • MinTapDuration,最小持续时间,只有超过这个时间SlowTap才会触发Performed
    • PressPoint,与前文同义。
  • 注意SlowTap的三个事件触发时机,在当前输入被视作SlowTap时触发Started,当前满足MinTapDuration之后松开按键时触发Performed,此时不会触发Canceled,如果没达到要求时间就松开,则触发Canceled

  • 使用案例如下:当按键时长小于0.2s时,视为Tap发射一颗子弹,当大于0.2s时进入SlowTap开始充能,超过0.5秒满足充能触发Performed,提前取消则触发Canceled

    //进入SlowTap 开始充能
    fireAction.started +=
    	ctx =>
    {
    	if (ctx.interaction is SlowTapInteraction)
    		m_Charging = true;
    };
    
    //开火
    fireAction.performed +=
        ctx =>
    {
        //如果是Tap,一次开火,发射一颗子弹
        if (ctx.interaction is TapInteraction)
        {
            Fire();
        }
        //如果是SlowTap,根据按键按住的充能时间,调用协程开火,持续至充能的时间长度
        else if (ctx.interaction is SlowTapInteraction)
        {
            StartCoroutine(BurstFire((int)(ctx.duration * burstSpeed)));
        }
        //因为如果SlowTap的performed触发,则不会再触发canceled所以需要在这里结束充能
        m_Charging = false;
    };
    
    fireAction.canceled +=
        ctx =>
    {
        m_Charging = false;
    };
    
Hold

image-20231210150202594

  • Hold 交互是用来检测一个按钮或按键是否被按住超过一个设定的时间阈值。它主要用于实现长按功能。当按键按下并保持超过指定的持续时间后,Hold 交互才会触发。这个交互通常用于那些需要长时间按压以触发的动作,比如在游戏中充能攻击或长按来显示菜单。

    关键特点:

    • 只有当按键按住超过设定的时间后才触发。
    • 适用于需要持续按住以触发特定功能的场景。
  • SlowTap具体有HoldTimePressPoint,两个参数来决定当前输入是否触发Hold

    • HoldTime当按住超过这个时间之后触发performed
  • SlowTap区别,最主要的区别在于:

    • Hold当你按住满足时间是就触发performed,而slowTap在满足时间之后松开时触发
    • Hold: 当使用 Hold 交互时,事件会在按键保持按下状态超过设定的时间阈值时触发 performed。也就是说,只要按键按住的时间足够长,不管最后是否释放按键,事件都会在达到时间阈值时触发。
    • SlowTap: 对于 SlowTap 交互,情况则略有不同。这里,事件的 performed 是在按键按下后保持一段设定时间,并且在这段时间结束后释放按键时触发。换句话说,SlowTap 需要玩家完整地执行按下、保持和释放的动作。
MultiTap

image-20231210150859623

  • 在Unity的Input System中,MultiTap 交互用于检测多次连续的点击或轻敲动作。这种交互允许你设置一个期望的点击次数和时间间隔,以识别例如双击、三击等行为。使用 MultiTap 交互,可以为游戏添加例如双击加速、三击施放特殊技能等功能。
  • 参数:
    • TapCount: 设置你希望玩家执行的点击次数。例如,对于双击操作,这个值应该设置为 2。
    • TapTime: 设置两次点击之间允许的最大间隔时间。如果玩家在此时间内未进行下一次点击,连续点击的计数将重置。
  • 当完成点击次数时,触发performed事件
Processors

image-20231210190230772

  • 在Unity的新输入系统中,InputActionProcessors 是一种强大的特性,它允许你在输入值被应用到游戏逻辑之前对这些值进行预处理。处理器(Processors)可以修改或转换输入数据,例如缩放、平滑、限制或者转换输入值的格式。下面是一些常用的 Processors 及其用途:
    1. Invert: 这个处理器会反转输入值。例如,一个正的轴值会变成负的,反之亦然。这在处理反向控制(如飞行模拟器中的倒飞)时非常有用。
    2. Normalize: 将输入值标准化到一定范围内,通常是 0 到 1 或 -1 到 1。这在处理模拟输入(如摇杆)时非常常见,可以确保不同设备的输入值在相同的范围内。
    3. Deadzone: 为模拟输入(如摇杆)设置一个“死区”,在这个区域内的输入值将被视为零。这有助于消除微小的输入波动,确保只有明显的输入才会被处理。
    4. Clamp: 将输入值限制在特定的最小值和最大值之间。这用于防止输入值超出预期的范围,比如确保角色的移动速度不会超过最大值。
    5. Scale: 将输入值乘以一个固定的因数。这可以用来增加或减少输入的影响,例如加倍玩家的移动速度。
    6. StickDeadzone: 专门用于游戏手柄摇杆的死区处理。它提供更精细的控制,特别适用于需要精确输入的游戏,如射击或飞行模拟游戏。
    7. CompensateForScreenOrientation: 主要用于触摸输入,以补偿屏幕旋转。这保证了即使设备的方向改变,触摸输入也能保持一致。
    8. AxisDeadzone: 类似于 Deadzone,但专门用于单一轴输入,如滑动条或触发器。

(3) UsingActionAsset

案例分析
案例README

随着越来越多动作的添加,手动设置和Enable/Disable所有动作可能变得非常繁琐。我们可以像这样在组件中使用InputActionMap

public class SimpleController : MonoBehaviour
{
    public InputActionMap actions;

    public void OnEnable()
    {
        actions.Enable();
    }

    public void OnDisable()
    {
        actions.Disable();
    }
}

这样子声明一个InputActionMap,则会在Inspector面板中显示一个Map,可以手动添加Action,以及对应的绑定,但是这样子做的话,在逻辑代码中需要手动查找对应的Action

更简单的方法是将所有动作放在一个单独的资产中,并生成一个C#包装类,这样代码补全也能自动为我们执行查找。而Unity中帮我们做了一个快捷的配置与C#包装类生成的方式,

具体步骤
  • 要创建这样的.inputactions资产,请在项目浏览器中右键单击并点击Create >> Input Actions。要编辑动作,请双击.inputactions资产,将弹出一个单独的窗口。

    image-20231211103502876
  • 选择资产的时候,在Inspector面板勾选generate C# class,这样编辑器就会为我们自动生成一个包装类。

    image-20231211103712325

  • 一个InputActionAsset,包含多个InputActionMap,一个Map包含一套Action,可以在一个ActionAsset中创建多个Map以对应不同输入需求映射,比如正常游戏时使用gameplay映射,进入载具后使用另一套carplay映射等

    image-20231211103534581
  • 使用上,即声明一个包装类对象m_Controlsm_Controls.gameplay即该资产下的gameplay map,在通过m_Controls.gameplay.fire调用具体的Action,其他使用方式与SimpleDemo_UsingActions一致

public class SimpleController_UsingActionAsset
{
    // This replaces the InputAction instances we had before with
    // the generated C# class.
    private SimpleControls m_Controls;

    //...

    public void Awake()
    {
        // To use the controls, we need to instantiate them.
        // This can be done arbitrary many times. E.g. there
        // can be multiple players each with its own SimpleControls
        // instance.
        m_Controls = new SimpleControls();

        // The generated C# class exposes all the action map
        // and actions in the asset by name. Here, we reference
        // the `fire` action in the `gameplay` action map, for
        // example.
        m_Controls.gameplay.fire.performed +=
        //...
    }

    //...

    public void Update()
    {
        // Same here, we can just look the actions up by name.
        var look = m_Controls.gameplay.look.ReadValue<Vector2>();
        var move = m_Controls.gameplay.move.ReadValue<Vector2>();

        //...
    }
}
代码注释
using System.Collections;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.InputSystem.Interactions;

// 使用动作集资产,而不是直接在组件上使用 InputActions。
public class SimpleController_UsingActionAsset : MonoBehaviour
{
    public float moveSpeed; // 定义移动速度
    public float rotateSpeed; // 定义旋转速度
    public float burstSpeed; // 定义爆发速度
    public GameObject projectile; // 定义投射物

    private SimpleControls m_Controls; // 定义控制器
    private bool m_Charging; // 定义是否正在充电的标志
    private Vector2 m_Rotation; // 定义旋转向量

    public void Awake()
    {
        m_Controls = new SimpleControls(); // 初始化控制器

        // 当 gameplay.fire 动作被执行时,执行以下代码
        m_Controls.gameplay.fire.performed +=
            ctx =>
        {
            // 如果交互是 SlowTapInteraction
            if (ctx.interaction is SlowTapInteraction)
            {
                // 开始协程,爆发火力,爆发时间为 ctx.duration * burstSpeed
                StartCoroutine(BurstFire((int)(ctx.duration * burstSpeed)));
            }
            else
            {
                // 否则,直接开火
                Fire();
            }
            // 设置充电状态为 false
            m_Charging = false;
        };

        // 当 gameplay.fire 动作开始时,执行以下代码
        m_Controls.gameplay.fire.started +=
            ctx =>
        {
            // 如果交互是 SlowTapInteraction,设置充电状态为 true
            if (ctx.interaction is SlowTapInteraction)
                m_Charging = true;
        };

        // 当 gameplay.fire 动作被取消时,设置充电状态为 false
        m_Controls.gameplay.fire.canceled +=
            ctx =>
        {
            m_Charging = false;
        };
    }

    public void OnEnable()
    {
        m_Controls.Enable(); // 启用控制器
    }

    public void OnDisable()
    {
        m_Controls.Disable(); // 禁用控制器
    }

    public void OnGUI()
    {
        // 如果正在充电,显示 "Charging..." 标签
        if (m_Charging)
            GUI.Label(new Rect(100, 100, 200, 100), "Charging...");
    }

    public void Update()
    {
        // 读取 look 和 move 的值
        var look = m_Controls.gameplay.look.ReadValue<Vector2>();
        var move = m_Controls.gameplay.move.ReadValue<Vector2>();

        // 先更新方向,然后移动。否则移动方向会延迟一帧。
        Look(look);
        Move(move);
    }

    private void Move(Vector2 direction)
    {
        // 如果方向的平方长度小于 0.01,不进行移动
        if (direction.sqrMagnitude < 0.01)
            return;
        var scaledMoveSpeed = moveSpeed * Time.deltaTime;
        // 为了简单起见,我们只在一个平面上进行移动。根据玩家的世界 Y 旋转来旋转方向。
        var move = Quaternion.Euler(0, transform.eulerAngles.y, 0) * new Vector3(direction.x, 0, direction.y);
        transform.position += move * scaledMoveSpeed;
    }

    private void Look(Vector2 rotate)
    {
        // 如果旋转的平方长度小于 0.01,不进行旋转
        if (rotate.sqrMagnitude < 0.01)
            return;
        var scaledRotateSpeed = rotateSpeed * Time.deltaTime;
        m_Rotation.y += rotate.x * scaledRotateSpeed;
        m_Rotation.x = Mathf.Clamp(m_Rotation.x - rotate.y * scaledRotateSpeed, -89, 89);
        transform.localEulerAngles = m_Rotation;
    }

    private IEnumerator BurstFire(int burstAmount)
    {
        // 爆发火力,每次开火后等待 0.1 秒
        for (var i = 0; i < burstAmount; ++i)
        {
            Fire();
            yield return new WaitForSeconds(0.1f);
        }
    }

    private void Fire()
    {
        // 创建新的投射物,设置其位置、旋转和大小,并给它一个随机的颜色
        var transform = this.transform;
        var newProjectile = Instantiate(projectile);
        newProjectile.transform.position = transform.position + transform.forward * 0.6f;
        newProjectile.transform.rotation = transform.rotation;
        const int size = 1;
        newProjectile.transform.localScale *= size;
        newProjectile.GetComponent<Rigidbody>().mass = Mathf.Pow(size, 3);
        newProjectile.GetComponent<Rigidbody>().AddForce(transform.forward * 20f, ForceMode.Impulse);
        newProjectile.GetComponent<MeshRenderer>().material.color =
            new Color(Random.value, Random.value, Random.value, 1.0f);
    }
}

(4) UsingPlayerInput

案例分析
案例README

最后,我们到达了输入系统的最高级别。虽然像上面的示例中那样编写输入可以快速且容易,但当游戏中可能有多个设备和/或多个玩家时,管理变得困难。这就是PlayerInput的用武之地。

PlayerInput会自动管理每个玩家的设备分配,并且还可以自动处理单人游戏中的控制方案切换(例如,当玩家在游戏手柄和鼠标&键盘之间切换时)。

在我们的案例中,由于我们没有控制方案或多个玩家,因此我们没有太多收获,但仍然,让我们看一看。

您可能首先注意到的是,现在Player对象上有两个脚本组件,一个是通常的SimpleController,另一个是PlayerInput。后者现在引用SimpleControls.inputactions。它还将gameplay设置为Default Action Map,以便在PlayerInput本身启用时立即启用游戏玩法动作。

为了获取回调,我们选择了Invoke Unity Events作为Behavior。如果您在检查器中展开Events折叠部分,您可以看到OnFireOnMoveOnLook被添加到相应的事件中。这里的每个回调方法看起来就像我们之前在fireAction上看到的startedperformedcanceled回调。

image-20231211110534802

具体步骤
  • 创建一个InputActionAsset配置好相关mapAction以及对应的绑定,如SimpleControls

    image-20231211103534581
  • Player添加组件PlayerInput,把InputAsset挂载上去,默认映射设置为gameplayBehavior推荐设置成Invoke Unity Event,其他如SendMessage等可能涉及查询函数等性能问题并且UnityEvent更加可视化。

    image-20231211110534802

  • 逻辑代码中写好对应的需要的回调函数

    public class SimpleController_UsingPlayerInput : MonoBehaviour
    {
        private Vector2 m_Move;
        //...
    
        public void OnMove(InputAction.CallbackContext context)
        {
            m_Move = context.ReadValue<Vector2>();
        }
    
        //...
    }
    
  • 然后再对应你设计好的Action事件中挂载你的回调函数

代码注释
using System.Collections;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.InputSystem.Interactions;

// 使用PlayerInput组件来设置输入的类。这个类处理角色的移动、旋转和射击动作。
public class SimpleController_UsingPlayerInput : MonoBehaviour
{
    // 公共变量,用于控制移动速度、旋转速度和射击的爆发速度。
    public float moveSpeed;
    public float rotateSpeed;
    public float burstSpeed;
    public GameObject projectile; // 射击时生成的弹丸预制体。

    // 私有变量,用于跟踪充能状态、旋转和移动的输入向量。
    private bool m_Charging; // 充能状态标志。
    private Vector2 m_Rotation; // 角色旋转的向量。
    private Vector2 m_Look; // 视角的输入向量。
    private Vector2 m_Move; // 移动的输入向量。

    // 当移动动作被触发时由PlayerInput组件调用。
    public void OnMove(InputAction.CallbackContext context)
    {
        // 从输入中读取值并存储到m_Move中。
        m_Move = context.ReadValue<Vector2>();
    }

    // 当观察动作被触发时由PlayerInput组件调用。
    public void OnLook(InputAction.CallbackContext context)
    {
        // 从输入中读取值并存储到m_Look中。
        m_Look = context.ReadValue<Vector2>();
    }

    // 当射击动作被触发时由PlayerInput组件调用。
    public void OnFire(InputAction.CallbackContext context)
    {
        switch (context.phase)
        {
            case InputActionPhase.Performed:
                // 检查交互是否为SlowTap(长按)
                if (context.interaction is SlowTapInteraction)
                {
                    // 根据按压时长执行爆发式射击。
                    StartCoroutine(BurstFire((int)(context.duration * burstSpeed)));
                }
                else
                {
                    // 对于普通点击,执行单次射击。
                    Fire();
                }
                m_Charging = false;
                break;

            case InputActionPhase.Started:
                // 如果是SlowTap交互开始,设置充能状态为true。
                if (context.interaction is SlowTapInteraction)
                    m_Charging = true;
                break;

            case InputActionPhase.Canceled:
                // 如果动作被取消,设置充能状态为false。
                m_Charging = false;
                break;
        }
    }

    // 在GUI上显示充能状态。
    public void OnGUI()
    {
        if (m_Charging)
            // 当充能为true时,在屏幕上显示“Charging...”。
            GUI.Label(new Rect(100, 100, 200, 100), "Charging...");
    }

    // 每帧调用一次的Update方法。
    public void Update()
    {
        // 根据输入值更新角色的朝向和移动。
        // 先更新朝向,确保与移动同步。
        Look(m_Look);
        Move(m_Move);
    }

    // 根据输入方向处理角色的移动。
    private void Move(Vector2 direction)
    {
        // 如果方向向量几乎为零,则不移动。
        if (direction.sqrMagnitude < 0.01)
            return;
        // 根据帧率独立变量调整移动速度。
        var scaledMoveSpeed = moveSpeed * Time.deltaTime;
        // 将2D方向向量转换为3D向量并应用旋转。
        var move = Quaternion.Euler(0, transform.eulerAngles.y, 0) * new Vector3(direction.x, 0, direction.y);
        // 应用计算出的移动向量。
        transform.position += move * scaledMoveSpeed;
    }

    // 根据输入处理角色的旋转。
    private void Look(Vector2 rotate)
    {
        // 如果旋转向量几乎为零,则不旋转。
        if (rotate.sqrMagnitude < 0.01)
            return;
        // 根据帧率独立变量调整旋转速度。
        var scaledRotateSpeed = rotateSpeed * Time.deltaTime;
        // 应用水平(偏航)和垂直(俯仰)旋转。
        m_Rotation.y += rotate.x * scaledRotateSpeed;
        m_Rotation.x = Mathf.Clamp(m_Rotation.x - rotate.y * scaledRotateSpeed, -89, 89);
        // 将计算出的旋转应用到变换上。
        transform.localEulerAngles = m_Rotation;
    }

    // 协程来处理爆发式射击。
    private IEnumerator BurstFire(int burstAmount)
    {
        // 根据burstAmount循环发射弹丸。
        for (var i = 0; i < burstAmount; ++i)
        {
            // 对每次射击调用Fire方法。
            Fire();
            // 每次射击之间等待一段时间。
            yield return new WaitForSeconds(0.1f);
        }
    }

    // 实例化并发射一个弹丸。
    private void Fire()
    {
        // 在当前位置和方向实例化弹丸。
        var newProjectile = Instantiate(projectile);
        newProjectile.transform.position = transform.position + transform.forward * 0.6f;
        newProjectile.transform.rotation = transform.rotation;
        
        // 设置弹丸的大小并调整其刚体的质量。
        const int size = 1;
        newProjectile.transform.localScale *= size;
        newProjectile.GetComponent<Rigidbody>().mass = Mathf.Pow(size, 3);
        // 给弹丸施加向前的冲击力。
        newProjectile.GetComponent<Rigidbody>().AddForce(transform.forward * 20f, ForceMode.Impulse);
        // 设置弹丸的颜色为随机颜色。
        newProjectile.GetComponent<MeshRenderer>().material.color =
            new Color(Random.value, Random.value, Random.value, 1.0f);
    }
}

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值