前言
在 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,核心原因是:
- MonoBehaviour 和 Unity 场景强绑定
- 业务逻辑不一定需要挂载到 GameObject 上
- 普通 C# 类更容易复用和测试
- 接口可以降低模块之间的耦合
- 接口有利于扩展、替换和维护
- MonoBehaviour 更适合做表现层、输入层和场景组件
简单来说:
MonoBehaviour 负责连接 Unity,接口和普通 C# 类负责处理业务。
在小项目中,直接使用 MonoBehaviour 没有问题;但在中大型项目中,合理使用接口和普通 C# 类,可以让项目结构更清晰、可维护性更高,也更符合面向对象设计思想。