Unity架构设计:使用接口解耦 GameWorld 与 NPC/Player 的关系
在 Unity 项目的架构设计中,我们经常会遇到如下需求:
GameWorld
管理整个游戏运行环境。Player
和多个NPC
在世界中交互。- 子对象(如
NPC
)需要访问GameWorld
的某些方法或数据。
如果直接将 GameWorld
引用传递给 NPC
,虽然可以快速实现功能,但很容易导致模块之间强耦合,使得后期维护、测试和扩展变得困难。
本文将分享一种更为清晰、可维护、易于扩展的解决方案 —— 通过接口进行上下文解耦设计。
一、问题背景
典型的设计初始结构如下:
public class GameWorld
{
public Player player;
public List<NPC> npcs;
public void Initialize()
{
player = new Player();
npcs = new List<NPC> { new NPC(), new NPC() };
}
public void SomeWorldMethod() { }
}
NPC
需要访问 GameWorld.SomeWorldMethod()
,那么常见方式有:
- ✅ 将
GameWorld
引用传入NPC
- ❌ 使用单例
GameWorld.Instance
这些方法虽然可行,但没有很好地实现模块解耦。
二、解耦设计方案:引入 IGameContext
接口
我们引入一个接口,来代表 GameWorld
提供给子对象的访问能力。
接口定义
public interface IGameContext
{
Player GetPlayer();
Vector3 GetSpawnPoint();
IEventDispatcher GetEventDispatcher();
}
这样,NPC
只依赖这个接口,而不是整个 GameWorld
类。
三、GameWorld 的接口实现
我们将实际的 GameWorld
封装成一个上下文对象 GameWorldContext
,实现接口:
public class GameWorldContext : IGameContext
{
private GameWorld world;
private IEventDispatcher dispatcher;
public GameWorldContext(GameWorld world)
{
this.world = world;
this.dispatcher = new DefaultEventDispatcher();
}
public Player GetPlayer() => world.GetPlayer();
public Vector3 GetSpawnPoint() => world.GetSpawnPoint();
public IEventDispatcher GetEventDispatcher() => dispatcher;
}
四、子系统的使用方式(以 NPC 为例)
public class NPC
{
private readonly IGameContext context;
public NPC(IGameContext context)
{
this.context = context;
}
public void Act()
{
var player = context.GetPlayer();
var spawn = context.GetSpawnPoint();
context.GetEventDispatcher().Dispatch("NPC_Acted", this);
}
}
此时 NPC
与 GameWorld
已完全解耦,仅通过 IGameContext
接口交互。
五、事件系统(可选扩展)
为了进一步解耦系统间通信,我们引入 IEventDispatcher
:
public interface IEventDispatcher
{
void Dispatch(string eventName, object data = null);
void Subscribe(string eventName, Action<object> callback);
void Unsubscribe(string eventName, Action<object> callback);
}
用于 NPC
向外广播行为、GameWorld
接收并处理事件。
六、测试支持:MockGameContext
有了解耦接口后,单元测试也变得轻松简单:
public class MockGameContext : IGameContext
{
public Player GetPlayer() => new GameObject("MockPlayer").AddComponent<Player>();
public Vector3 GetSpawnPoint() => Vector3.zero;
public IEventDispatcher GetEventDispatcher() => new DefaultEventDispatcher();
}
七、目录结构建议
为了更好地组织代码,可以采用如下项目结构:
Game/
├── Interfaces/
│ ├── IGameContext.cs
│ └── IEventDispatcher.cs
├── GameWorld/
│ ├── GameWorld.cs
│ └── GameWorldContext.cs
├── Characters/
│ ├── Player.cs
│ ├── NPC.cs
├── Events/
│ └── DefaultEventDispatcher.cs
└── Tests/
└── MockGameContext.cs
八、优点总结
优点 | 描述 |
---|---|
✅ 高内聚低耦合 | 子系统只依赖接口,不依赖具体实现 |
✅ 易于扩展 | 更换 GameWorld 实现或添加 PVPWorld 不影响子系统 |
✅ 易于测试 | 可以用 MockContext 进行单元测试 |
✅ 更强可维护性 | 系统职责边界明确,便于多人协作 |
九、结语
这套通过接口实现解耦的设计思想,适用于多数 Unity 中的子系统开发,如:
- 任务系统访问世界状态
- AI 系统感知环境
- 技能系统调用全局事件
随着项目复杂度提升,良好的架构设计会显著提升开发效率与维护性。如果你有类似的解耦需求,不妨试试这套接口架构设计。