维度解耦:构建工业级 Unity 事件驱动与新版输入架构
本文将通过 Input System + 全局事件总线 (Event Bus) 的实战案例,解析如何构建一个高内聚、低耦合的现代化游戏骨架。
一、 为什么需要这套系统?
传统的 Input.GetKeyDown 会导致逻辑分散在各个脚本的 Update 中,难以管理模式切换(如上帝视角与英雄视角切换)。而"单例全局事件系统"则解决了以下痛点:
- 零耦合通信:发送者不需要认识接收者。
- 状态同步:游戏状态(准备/战斗)或模式(英雄/指挥官)只需在一个地方更改,全场自动响应。
- 输入抽象:将"按下空格键"抽象为"跳跃信号",无论输入源是键盘、手柄还是手机。
二、完整实现过程(包括新版输入系统的使用)
- 创建一个新的U3d项目,以及在project窗口创建好相关目录(特别是Scripts)
- 在Scripts目录下创建枚举类GameState,定义游戏状态类型
csharp
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public enum GameState
{
Prepare, // 准备阶段:造塔、选位
Battle, // 战斗阶段:敌人生成
Pause, // 暂停阶段
GameOver // 结束阶段
}
- 创建全局事件管理中心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();
}
}
- 创建全局游戏配置中心并且设置为单例模式
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);
}
}
}
-
使用新版输入系统:

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

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

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


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

配置完后如图

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

- 编写 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();
}
}
}
- 在创建两个空物体GameManager和InputSystem_Manager,分别把GameManager脚本和InputReader脚本绑在上面



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


- 创建玩家角色脚本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;
}
- 记得把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. 物理平滑性
通过在 InputReader 的 Update 中轮询 ReadValue<Vector2>,而不是依赖事件回调,我们获得了与帧率对齐的位移数据。这解决了"事件回调触发频率不稳定"导致的移动卡顿问题。
2. 可自主扩展
由于采用了广播机制,小怪的逻辑可以独立存在。
- 如果需要玩家命令小怪跳:让小怪订阅广播。
- 如果需要小怪自主跳 :在小怪脚本中用
Raycast探测,检测到障碍后直接调用自身的Jump()方法。
无论哪种方式,都不需要修改InputReader。
3. 生命周期安全
代码中严格执行了 OnEnable 订阅、OnDisable 取消订阅的原则:
csharp
private void OnEnable()
{
// 挂载到天线:监听跳跃信号
EventManager.OnHeroJumpRequest += Jump;
}
private void OnDisable()
{
EventManager.OnHeroJumpRequest -= Jump; // 必须断开天线,防止对象被销毁后报错
}
这是防止 Unity 开发中常见的 "空引用异常 (NullReferenceException)" 和 "内存泄漏" 的关键法则。