Unity全局事件中心与新版输入架构实现练习——上帝模式与英雄模式的输入系统映射切换

维度解耦:构建工业级 Unity 事件驱动与新版输入架构

本文将通过 Input System + 全局事件总线 (Event Bus) 的实战案例,解析如何构建一个高内聚、低耦合的现代化游戏骨架。


一、 为什么需要这套系统?

传统的 Input.GetKeyDown 会导致逻辑分散在各个脚本的 Update 中,难以管理模式切换(如上帝视角与英雄视角切换)。而"单例全局事件系统"则解决了以下痛点:

  1. 零耦合通信:发送者不需要认识接收者。
  2. 状态同步:游戏状态(准备/战斗)或模式(英雄/指挥官)只需在一个地方更改,全场自动响应。
  3. 输入抽象:将"按下空格键"抽象为"跳跃信号",无论输入源是键盘、手柄还是手机。

二、完整实现过程(包括新版输入系统的使用)

  1. 创建一个新的U3d项目,以及在project窗口创建好相关目录(特别是Scripts)
  2. 在Scripts目录下创建枚举类GameState,定义游戏状态类型
csharp 复制代码
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public enum GameState
{
    Prepare,  // 准备阶段:造塔、选位
    Battle,   // 战斗阶段:敌人生成
    Pause,    // 暂停阶段
    GameOver  // 结束阶段
}
  1. 创建全局事件管理中心EventManager
csharp 复制代码
using System;

namespace Project.Core
{
    // 全局事件管理器,负责所有模块的事件通信
    public static class EventManager
    {
        // 游戏状态切换事件
        public static event Action<GameState> OnStateChanged;
        // 触发游戏状态改变
        public static void CallStateChanged(GameState s) => OnStateChanged?.Invoke(s);
        
        // --- 视角切换事件 (True = 英雄模式, False = 指挥官模式) ---
        public static event Action<bool> OnViewModeChanged;
        public static void CallViewModeChanged(bool isHero) => OnViewModeChanged?.Invoke(isHero);
        
        // --- 英雄跳跃动作事件 ---
        public static event Action OnHeroJumpRequest; 
        public static void CallHeroJumpRequest() => OnHeroJumpRequest?.Invoke();
    }
}
  1. 创建全局游戏配置中心并且设置为单例模式
csharp 复制代码
using UnityEngine;

namespace Project.Core
{
    public class GameManager : MonoBehaviour
    {
        // 设置单例模式,全局唯一访问点
        public static GameManager Instance { get; private set; }

        // 当前游戏数据(外部只能读,不能改)
        public GameState CurrentState { get; private set; }

        private void Awake()
        {
            // 单例初始化,确保全局唯一
            if (Instance == null)
                Instance = this;
            else
                Destroy(gameObject);
        }

        private void Start()
        {
            // 游戏开始,进入准备阶段
            ChangeState(GameState.Prepare);
        }

        // 切换游戏状态(准备/战斗/暂停/结束)
        public void ChangeState(GameState newState)
        {
            // 状态相同不重复执行
            if (CurrentState == newState) return;

            CurrentState = newState;

            // 暂停/游戏结束时停止游戏运行
            Time.timeScale = (newState == GameState.Pause || newState == GameState.GameOver) ? 0 : 1;

            // 调用发布消息的方法通知所有系统状态已改变
            EventManager.CallStateChanged(newState);
        }
    }
}
  1. 使用新版输入系统:

    ① 找到Assets/Settings/ 目录下 右键 -> Create -> Input Actions,命名为 GameInputControls。

    ②生成代码:单击点击该文件,在 Inspector 面板勾选 Generate C# Class 并点击 Apply。

    ③配置按键:双击打开文件,点击左侧的 + 号创建两个 Action Maps,并按照下表添加 Actions:


    注意:Hero的Move绑定的是图下图第二个
    配置path可以用listen去监听按键

    配置完后如图

    配置好后一定要点击右上角的Save Asset!!!!

  1. 编写 InputReader 核心脚本,负责监听上面配置的所有动作。
csharp 复制代码
using UnityEngine;
using UnityEngine.InputSystem;
using Project.Core;

namespace Project.Systems
{
    public class InputReader : MonoBehaviour, GameInputControls.ICommanderActions, GameInputControls.IHeroActions
    {
        private GameInputControls _controls;
        private bool _isHeroMode = false;

        public Vector2 MoveInput { get; private set; }
        private void Awake()
        {
            _controls = new GameInputControls();
            _controls.Commander.SetCallbacks(this);
            _controls.Hero.SetCallbacks(this);
            SetInputMode(false);
        }

        private void SetInputMode(bool isHero)
        {
            if (isHero)
            {
                _controls.Commander.Disable();
                _controls.Hero.Enable();
            }
            else
            {
                _controls.Hero.Disable();
                _controls.Commander.Enable();
            }
        }
        
        private void Update()
        {
            // 如果是英雄模式,每帧主动读取值
            if (_isHeroMode)
            {
                // 直接从刚刚创建好的 Action 中读取当前帧的实时数值
                MoveInput = _controls.Hero.Move.ReadValue<Vector2>();

                // 仅用于调试日志(建议实际开发时注释掉,否则每帧都打日志会卡顿)
                if (MoveInput.sqrMagnitude > 0)
                {
                    // Debug.Log($"持续读取移动: {MoveInput}");
                }
            }
            else
            {
                MoveInput = Vector2.zero;
            }
        }

        // ----------------- 共有逻辑:切换视角 (Tab) -----------------
        public void OnToggleView(InputAction.CallbackContext context)
        {
            if (context.performed)
            {
                _isHeroMode = !_isHeroMode;
                SetInputMode(_isHeroMode);

                EventManager.CallViewModeChanged(_isHeroMode);
                Debug.Log($"<color=orange>[Input] 模式切换: {(_isHeroMode ? "英雄" : "指挥官")}</color>");
            }
        }

        // ----------------- 英雄模式 (IHeroActions) -----------------
        public void OnMove(InputAction.CallbackContext context) { }

        public void OnSpace(InputAction.CallbackContext context)
        {
            // 只有在按下的一瞬间 (performed) 且是英雄模式时发送信号
            if (context.performed && _isHeroMode) 
            {
                // 广播跳跃请求
                EventManager.CallHeroJumpRequest();
                Debug.Log("<color=green>[广播] 收到空格输入,向全场发送跳跃请求!</color>");
            }
        }

        public void OnAttack(InputAction.CallbackContext context)
        {
            if (context.performed && _isHeroMode) 
            {
                Debug.Log("<color=red>英雄攻击!</color>");
            }
        }

        // ----------------- 指挥官模式 (ICommanderActions) -----------------
        public void OnClick(InputAction.CallbackContext context)
        {
            if (context.performed && !_isHeroMode) 
            {
                Debug.Log("<color=cyan>指挥官点击!</color>");
            }
        }

        // ----------------- 生命周期管理 -----------------
        private void OnEnable()
        {
            // 启用时根据当前模式激活正确的 Map,而不是全部 Enable
            SetInputMode(_isHeroMode);
        }

        private void OnDisable()
        {
            _controls?.Disable();
        }

        private void OnDestroy()
        {
            _controls?.Dispose();
        }
    }
}
  1. 在创建两个空物体GameManager和InputSystem_Manager,分别把GameManager脚本和InputReader脚本绑在上面

  2. 创建一个Plane作为地面以及一个胶囊体作为角色,角色要添加一个Rigidbody组件,并且冻结旋转x和z轴的旋转,避免摔倒

  3. 创建玩家角色脚本HeroController,并且引入新输入系统刚刚创建好的InputSystem_Manager
csharp 复制代码
using UnityEngine;
using Project.Systems;
using Project.Core; // 引入核心命名空间以使用 EventManager

[RequireComponent(typeof(Rigidbody))]
public class HeroController : MonoBehaviour
{
    [Header("引用设置")]
    [SerializeField] private InputReader inputReader; 
    
    [Header("移动参数")]
    [SerializeField] private float moveSpeed = 7f;
    [SerializeField] private float rotationSpeed = 15f;

    [Header("跳跃参数")]
    [SerializeField] private float jumpForce = 5f;
    [SerializeField] private bool isGrounded; 

    private Rigidbody _rb;

    private void Awake()
    {
        _rb = GetComponent<Rigidbody>();
    }
    private void OnEnable()
    {
        // 挂载到天线:监听跳跃信号
        EventManager.OnHeroJumpRequest += Jump;
    }

    private void OnDisable()
    {
        // 拔掉天线:停止监听(防止内存泄漏)
        EventManager.OnHeroJumpRequest -= Jump;
    }

    private void Update()
    {
        HandleMove();
    }

    private void HandleMove()
    {
    		 //获取使用inputReader脚本update方法中实时监听到的方向值值域为(-1, 1)
        Vector2 input = inputReader.MoveInput;
        //Vector3 (Direction):将意图映射到 3D 物理空间。
			  //input.x 映射到世界坐标的 X(左右)。
			  //input.y 映射到世界坐标的 Z(前后)。
        Vector3 moveDir = new Vector3(input.x, 0, input.y);
				//通过moveDir的向量的长度平方去判断是否有移动
        if (moveDir.sqrMagnitude > 0.001f)
        {
            transform.Translate(moveDir * (moveSpeed * Time.deltaTime), Space.World);
            //计算出角色脸应该朝向移动方向(四元数)。
            Quaternion targetRot = Quaternion.LookRotation(moveDir);
            //平滑旋转朝向
            transform.rotation = Quaternion.Slerp(transform.rotation, targetRot, rotationSpeed * Time.deltaTime);
        }
    }

    // 现在这个方法由 EventManager 广播触发
    public void Jump()
    {
        if (isGrounded)
        {
            _rb.AddForce(Vector3.up * jumpForce, ForceMode.Impulse);
            Debug.Log("<color=yellow>[英雄] 收到跳跃广播,执行物理跳跃!</color>");
            isGrounded = false; // 起跳瞬间设为非接地,防止连续跳
        }
    }

    // 落地检测
    private void OnCollisionStay(Collision collision) => isGrounded = true;
    private void OnCollisionExit(Collision collision) => isGrounded = false;
}
  1. 记得把InputSystem_Manager拖入到玩家代码的引用里

三、效果演示

unity新版输入系统角色控制移动

四、 核心架构拆解

这套架构由三部分组成:信号中心 (EventManager)感知大脑 (InputReader)执行躯干 (HeroController)

1. 信号中心:EventManager (广播站)

使用 C# 的 event 关键字构建。它像一个无限电台,负责定义"频道"。

csharp 复制代码
public static class EventManager {
    // 动作频道:跳跃请求
    public static event Action OnHeroJumpRequest; 
    public static void CallHeroJumpRequest() => OnHeroJumpRequest?.Invoke();

    // 状态频道:模式切换
    public static event Action<bool> OnViewModeChanged;
    public static void CallViewModeChanged(bool isHero) => OnViewModeChanged?.Invoke(isHero);
}

2. 感知大脑:InputReader (输入抽象)

基于 Unity 新版 Input System。它的职责是解析输入并决定是否发报

  • 持续读取 (Polling) :对于移动(Move),每帧读取 Vector2 值并缓存,供物理层直接读取。
  • 瞬时触发 (Callback) :对于跳跃(Space),在 performed 阶段通过 EventManager 发射信号。
  • 权限管理 :通过 _isHeroMode 锁死输入分发。在指挥官模式下,英雄动作的信号被物理拦截。

3. 执行躯干:HeroController (物理层)

它不关心按键,它只关心"信号""数据"。

  • 订阅机制 :在 OnEnable 时订阅 OnHeroJumpRequest
  • 物理实现 :接收到信号后,调用 Rigidbody.AddForce 实现物理反馈。

五、 深度分析:双模式切换逻辑

csharp 复制代码
private void SetInputMode(bool isHero) {
    if (isHero) {
        _controls.Commander.Disable(); // 禁用指挥官操作
        _controls.Hero.Enable();       // 开启英雄操作
    } else {
        _controls.Hero.Disable();
        _controls.Commander.Enable();
    }
}

这种设计在 Action Map 层面直接切断了非法输入。

  • 在英雄视角下:点击地面不会触发指挥官的寻路。
  • 在指挥官视角下:按空格键英雄不会原地起跳。

这不仅节省了性能(禁用的 Map 不消耗 CPU),更从底层规避了逻辑冲突。


六、 核心优势总结

1. 物理平滑性

通过在 InputReaderUpdate 中轮询 ReadValue<Vector2>,而不是依赖事件回调,我们获得了与帧率对齐的位移数据。这解决了"事件回调触发频率不稳定"导致的移动卡顿问题。

2. 可自主扩展

由于采用了广播机制,小怪的逻辑可以独立存在。

  • 如果需要玩家命令小怪跳:让小怪订阅广播。
  • 如果需要小怪自主跳 :在小怪脚本中用 Raycast 探测,检测到障碍后直接调用自身的 Jump() 方法。
    无论哪种方式,都不需要修改 InputReader

3. 生命周期安全

代码中严格执行了 OnEnable 订阅、OnDisable 取消订阅的原则:

csharp 复制代码
private void OnEnable()
{
        // 挂载到天线:监听跳跃信号
        EventManager.OnHeroJumpRequest += Jump;
}
private void OnDisable() 
{
    EventManager.OnHeroJumpRequest -= Jump; // 必须断开天线,防止对象被销毁后报错
}

这是防止 Unity 开发中常见的 "空引用异常 (NullReferenceException)""内存泄漏" 的关键法则。


七、 结语

这套架构将 "输入检测 -> 信号传递 -> 行为执行" 彻底剥离。它让你的游戏不仅仅是一个能跑能跳的方块,而是一个拥有清晰层级结构的软件系统。

相关推荐
小新同学^O^1 小时前
简单学习Spring原理
java·学习·spring
戴西软件2 小时前
戴西软件入选2026年安徽省制造业数智化转型服务商名单
java·大数据·服务器·前端·人工智能
爱棋笑谦2 小时前
springboot—数据源相关配置
java·spring boot·spring
踩着两条虫10 小时前
「AI + 低代码」的可视化设计器
开发语言·前端·低代码·设计模式·架构
budingxiaomoli11 小时前
Spring IoC &DI
java·spring·ioc·di
Spider Cat 蜘蛛猫11 小时前
Springboot SSO系统设计文档
java·spring boot·后端
未若君雅裁11 小时前
MySQL高可用与扩展-主从复制读写分离分库分表
java·数据库·mysql
学习中.........11 小时前
从扰动函数的变化,感受红黑树带来的性能提升
java
计算机安禾11 小时前
【c++面向对象编程】第24篇:类型转换运算符:自定义隐式转换与explicit
java·c++·算法