Unity 中为什么业务脚本一般继承接口,而不是直接继承 MonoBehaviour?

前言

在 Unity 开发中,很多初学者会习惯性地让所有脚本都继承 MonoBehaviour:

csharp 复制代码
  public class PlayerManager : MonoBehaviour
  {
  }

因为只有继承 MonoBehaviour,脚本才能挂载到 GameObject 上,也才能使用 Start()、Update()、Coroutine、SerializeField 等 Unity 提供的能力。

但是在实际项目中,尤其是中大型项目里,很多"业务脚本"并不会直接继承 MonoBehaviour,而是更倾向于继承接口,例如:

csharp 复制代码
  public interface ILoginService
  {
      void Login(string account, string password);
  }
csharp 复制代码
  public class LoginService : ILoginService
  {
      public void Login(string account, string password)
      {
          // 登录业务逻辑
      }
  }

那么问题来了:

为什么业务脚本一般更推荐继承接口,而不是直接继承 MonoBehaviour?


一、MonoBehaviour 是什么?

MonoBehaviour 是 Unity 提供的基础脚本类。

只要一个类继承了 MonoBehaviour,它就可以作为组件挂载到 GameObject 上,并且能够使用 Unity 的生命周期函数:

csharp 复制代码
  public class PlayerController : MonoBehaviour
  {
      private void Awake()
      {
      }

      private void Start()
      {
      }

      private void Update()
      {
      }
  }

常见能力包括:

  • 可以挂载到 GameObject 上
  • 可以使用 Awake、Start、Update 等生命周期函数
  • 可以使用协程 StartCoroutine
  • 可以通过 Inspector 暴露字段
  • 可以访问 transform、gameObject 等 Unity 对象

所以,MonoBehaviour 更适合做"Unity 场景组件"。


二、业务脚本是什么?

业务脚本一般指的是和游戏规则、数据处理、流程控制相关的逻辑。

例如:

  • 登录逻辑
  • 背包系统
  • 任务系统
  • 战斗结算
  • 商城购买
  • 存档读取
  • 网络请求
  • 数据解析
  • 角色升级规则

这些逻辑本质上不一定依赖 Unity 场景,也不一定需要挂载到 GameObject 上。

例如,一个背包系统可能只是处理数据:

csharp 复制代码
  public class InventoryService
  {
      private List<Item> items = new List<Item>();

      public void AddItem(Item item)
      {
          items.Add(item);
      }

      public void RemoveItem(Item item)
      {
          items.Remove(item);
      }
  }

这个类并不需要 Update(),也不需要挂到某个物体上。


三、为什么业务脚本不推荐直接继承 MonoBehaviour?

1. MonoBehaviour 和 Unity 场景强绑定

继承 MonoBehaviour 的类必须依附于 GameObject 才能正常工作。

例如:

csharp 复制代码
  public class LoginManager : MonoBehaviour
  {
      public void Login()
      {
          Debug.Log("登录");
      }
  }

如果想使用它,通常需要:

csharp 复制代码
  LoginManager loginManager = gameObject.AddComponent<LoginManager>();

或者提前挂在场景中的某个对象上。

这会导致业务逻辑和 Unity 场景绑定得太紧。

业务代码本来只是处理登录、背包、任务等逻辑,却被迫依赖 GameObject、场景和组件生命周期。

这样会降低代码的灵活性。

2. 不方便单元测试

普通 C# 类可以直接 new:

csharp 复制代码
  ILoginService loginService = new LoginService();
  loginService.Login("admin", "123456");

但是 MonoBehaviour 不能直接这样创建:

csharp 复制代码
  LoginManager loginManager = new LoginManager(); // 不推荐,也无法正常作为 Unity 组件使用

MonoBehaviour 需要通过 Unity 的组件系统创建:

csharp 复制代码
  LoginManager loginManager = gameObject.AddComponent<LoginManager>();

这意味着测试业务逻辑时,你需要依赖 Unity 环境。

而如果业务类只继承接口,就可以脱离 Unity 运行,更容易写单元测试。

3. 职责不清晰

MonoBehaviour 更像是"表现层"或"场景层"的组件。

例如:

csharp 复制代码
  public class PlayerView : MonoBehaviour
  {
      public Text hpText;

      public void UpdateHp(int hp)
      {
          hpText.text = hp.ToString();
      }
  }

它负责和 Unity 对象、UI、动画、特效交互。

而业务逻辑应该更关注规则:

csharp 复制代码
  public class PlayerHpService : IPlayerHpService
  {
      public int CalculateDamage(int hp, int damage)
      {
          return Math.Max(0, hp - damage);
      }
  }

如果所有逻辑都写进 MonoBehaviour,很容易变成这样:

csharp 复制代码
  public class PlayerManager : MonoBehaviour
  {
      // UI
      public Text hpText;

      // 数据
      private int hp;

      // 输入
      private void Update()
      {
          if (Input.GetKeyDown(KeyCode.Space))
          {
              TakeDamage(10);
          }
      }

      // 业务
      private void TakeDamage(int damage)
      {
          hp -= damage;
          hpText.text = hp.ToString();
      }
  }

这个类同时负责输入、数据、业务、UI,后期会越来越难维护。

4. 接口更利于解耦

接口只定义能力,不关心具体实现。

例如:

csharp 复制代码
  public interface IAudioService
  {
      void PlaySound(string soundName);
  }

具体实现可以有多个:

csharp 复制代码
  public class UnityAudioService : IAudioService
  {
      public void PlaySound(string soundName)
      {
          Debug.Log("播放音效:" + soundName);
      }
  }

业务代码依赖接口:

csharp 复制代码
  public class BattleService
  {
      private readonly IAudioService audioService;

      public BattleService(IAudioService audioService)
      {
          this.audioService = audioService;
      }

      public void Attack()
      {
          audioService.PlaySound("attack");
      }
  }

这样 BattleService 并不关心音效到底是 Unity 播放的,还是测试环境中模拟的。

这就是面向接口编程。

优点是:

  • 降低模块之间的依赖
  • 替换实现更方便
  • 代码更容易扩展
  • 测试时可以使用假对象 Mock

四、接口和 MonoBehaviour 的职责区别

可以简单理解为:

类型 适合做什么
MonoBehaviour 场景组件、UI、动画、输入、碰撞、生命周期
接口 + 普通 C# 类 业务规则、数据处理、系统逻辑、服务模块

例如:

推荐结构

csharp 复制代码
  public interface IInventoryService
  {
      void AddItem(Item item);
      void RemoveItem(Item item);
  }

  public class InventoryService : IInventoryService
  {
      private List<Item> items = new List<Item>();

      public void AddItem(Item item)
      {
          items.Add(item);
      }

      public void RemoveItem(Item item)
      {
          items.Remove(item);
      }
  }

  public class InventoryPanel : MonoBehaviour
  {
      private IInventoryService inventoryService;

      private void Start()
      {
          inventoryService = new InventoryService();
      }

      public void OnClickAddItem()
      {
          inventoryService.AddItem(new Item());
      }
  }

在这个结构中:

  • InventoryPanel 负责 Unity UI 交互
  • InventoryService 负责背包业务逻辑
  • IInventoryService 负责定义接口规范

这样职责更清楚。


五、是不是完全不能让业务脚本继承 MonoBehaviour?

不是。

并不是说业务脚本绝对不能继承 MonoBehaviour。

在小项目、Demo、原型开发中,把逻辑写在 MonoBehaviour 中是很常见的,也更简单。

例如:

csharp 复制代码
  public class DoorController : MonoBehaviour
  {
      public void OpenDoor()
      {
          gameObject.SetActive(false);
      }
  }

这种简单逻辑完全可以直接写。

但是当项目变大之后,如果大量业务逻辑都写在 MonoBehaviour 中,就容易出现以下问题:

  • 类越来越大
  • 依赖关系混乱
  • 很难复用
  • 很难测试
  • 修改一个功能影响多个模块
  • 场景对象丢失后逻辑也跟着出问题

所以更推荐把业务逻辑拆出来,用接口和普通 C# 类管理。


六、常见项目分层方式

一个比较常见的 Unity 项目结构是:

Scripts

├── Game

│ ├── UI

│ ├── View

│ └── Mono

├── Services

│ ├── ILoginService.cs

│ ├── LoginService.cs

│ ├── IInventoryService.cs

│ └── InventoryService.cs

├── Models

│ ├── PlayerData.cs

│ └── ItemData.cs

└── Managers

└── GameEntry.cs

大致职责如下:

层级 作用
UI / View 负责显示、点击、动画等 Unity 相关逻辑
Services 负责业务逻辑
Models 负责数据结构
Managers / Entry 负责模块初始化和调度

这样可以让 Unity 组件和业务逻辑分离。


七、一个简单例子:登录功能

不推荐写法

csharp 复制代码
 public class LoginPanel : MonoBehaviour
  {
      public InputField accountInput;
      public InputField passwordInput;

      public void OnClickLogin()
      {
          string account = accountInput.text;
          string password = passwordInput.text;

          if (string.IsNullOrEmpty(account))
          {
              Debug.Log("账号不能为空");
              return;
          }

          if (password.Length < 6)
          {
              Debug.Log("密码长度不足");
              return;
          }

          Debug.Log("发送登录请求");
      }
  }

这个类同时负责:

  • 获取 UI 输入
  • 校验账号密码
  • 登录业务逻辑
  • 输出结果

后期如果登录逻辑变化,这个 UI 类也要修改。

推荐写法

定义接口:

csharp 复制代码
  public interface ILoginService
  {
      bool CheckLoginInfo(string account, string password);
  }

实现业务逻辑:

csharp 复制代码
  public class LoginService : ILoginService
  {
      public bool CheckLoginInfo(string account, string password)
      {
          if (string.IsNullOrEmpty(account))
          {
              return false;
          }

          if (string.IsNullOrEmpty(password) || password.Length < 6)
          {
              return false;
          }

          return true;
      }
  }

UI 层调用业务层:

csharp 复制代码
  public class LoginPanel : MonoBehaviour
  {
      public InputField accountInput;
      public InputField passwordInput;

      private ILoginService loginService;

      private void Awake()
      {
          loginService = new LoginService();
      }

      public void OnClickLogin()
      {
          string account = accountInput.text;
          string password = passwordInput.text;

          bool success = loginService.CheckLoginInfo(account, password);

          if (success)
          {
              Debug.Log("登录信息合法");
          }
          else
          {
              Debug.Log("登录信息不合法");
          }
      }
  }

这样 LoginPanel 只负责 UI,LoginService 负责业务。


八、总结

Unity 中业务脚本一般更推荐继承接口,而不是直接继承 MonoBehaviour,核心原因是:

  1. MonoBehaviour 和 Unity 场景强绑定
  2. 业务逻辑不一定需要挂载到 GameObject 上
  3. 普通 C# 类更容易复用和测试
  4. 接口可以降低模块之间的耦合
  5. 接口有利于扩展、替换和维护
  6. MonoBehaviour 更适合做表现层、输入层和场景组件

简单来说:

MonoBehaviour 负责连接 Unity,接口和普通 C# 类负责处理业务。

在小项目中,直接使用 MonoBehaviour 没有问题;但在中大型项目中,合理使用接口和普通 C# 类,可以让项目结构更清晰、可维护性更高,也更符合面向对象设计思想。