使用 MVVM 开发
MVVM,也就是 Model / View / ViewModel 的一种设计思想,这样的设计思路适合结合 TDD + LLM Agent 进行开发。
Model:纯数据、逻辑层,负责网络与服务器消息收发。
View:表现层,只负责根据现有数据进行表现还原。
ViewModel:视图结构,或者视图模型层,属于当前视图展示的目标数据。
为什么要用 MVVM
在游戏开发中,当我们大量使用 Agent 开发后,一大问题就是 LLM 极难判定视图层上的信息。
视觉模型擅长的
-
语义化结构识别:这里有一个血条;
-
层级关系推断:文字在图片右边/内部;
-
样式还原:颜色、字体的近似还原;
视觉模型不擅长的
-
响应式布局:仅看图无法决策锚点设定;
-
精度误差:LLM 给出的像素是估算而非真实值;
-
复杂列表/网格:更可能识别成多个独立节点;
除此之外,我们实际开发中,表现异常的原因多种多样:可能是这个功能本身就没做,可能对应资源没有加载出来、配置问题、其他模块报错导致程序中断……即便有界面截图和日志,也不容易定位到具体问题。更不用说,如果直接让 LLM 识别真实 UI 的工作流过重,跑起来开销高。
因此我这里设计为 Agent 开发做 MVVM 架构设计,其核心原则是:所有视图逻辑都截止在 ViewModel 层,而 View 层(UI)实现去逻辑化,只忠诚反应 VM 的数据结果。
我举一个游戏开发中常用例子:玩家信息面板
这里需求较为单纯:能看到玩家信息(头像、名字等),然后看自己的信息和其他玩家的信息(路人、盟友、好友等)有不同的展示项目和状态变化。用户可以修改自己的信息,不能修改其他人的信息,但是可以对其他人点赞、举报、发起私聊等。
好,如果是我们没有做 VM 拆分,在 UI 层持有大量的逻辑判断,那我们如何验收面板逻辑呢?只有打开面板看一看,点点按钮这些。传统游戏开发的时候这没问题,本来也是人工开发必不可少的一步,但是接入 AI Agent 开发之后就有问题了,因为就是单纯打开这个 UI 面板,就暗含了大量业务逻辑:

在打开 UI 面板之前的每一步都有可能出问题:
-
游戏初始化失败,无法加载其他某个模块;
-
登陆失败:服务器关闭了、亦或者清数据了,无法拿到预期的账号数据;
-
登录后无法继续:触发了新手引导、服务器推送弹窗、突发事件通知、跳转到其他界面等阻断了 UI 交互逻辑;
-
打开UI面板:资源加载失败、找不到某个必须数据导致初始化中断;
-
服务器异常:某功能服务器没实现或者服务器报错,导致后续关联功能都无法开发、验证;
-
……
且不用说各种阻断问题导致无法测试,且很多问题靠客户端无法解决。我们平时开发也解决不了,只能尽可能方便测试,比如加 GM 命令、屏蔽一些功能、哀嚎让服务器同学启一个测试服、搞点复制账号等。即便上述问题都没有发生,这个流程也太慢了:本来启动 Unity 就慢(尤其是大项目),然后登录要等,进游戏要等,场景加载要等,服务器消息延迟要等,点击交互后看效果也要等。这些等待对人类来说无所谓,但对于 AI Agent 来讲就太慢了:光是等待进 Unity 的时间,就足够 AI 写个几百行代码了。
这显然不符合 AI 高频开发的应用场景。
对于 Vibe Coding 而言,什么东西是最好验收的?
答案只有一个:数据。
让 AI 看一张图,问他某个按钮是否显示出来了,很麻烦,费 Token,且不说还会有 AI 幻觉。但是问他当前代码中某个布尔值是否为 true,这个就容易多了。
因此我设计 VM 层,作为逻辑和表现的中转,这样做的好处有:
-
逻辑验收简化:面板开不开无所谓,只验收 Model 和 ViewModel 层的数据结果就行了。
-
视图层功能开发简化:只写 ViewModel 的胶水代码,几乎没有逻辑。
AI 写纯数据逻辑的效率非常高,AI 写纯视图胶水代码的效率也非常高。
当然这个架构的前提条件,就是视图层本身的执行要足够可靠。比如点击按钮就会触发回调,给图片组件赋值就会显示对应图片,网格布局会按照实际配置生效……在我们使用 Unity 这种现代游戏引擎,这一块的可靠性非常之高。
如何进行功能设计
我们以个实际案例作为示例:
背包页面
一个背包页面有多个页签,对应多个不同分类的道具。道具要显示对应的 Icon、数量,点击后还有 Tips 提示。选中一个道具后,则可以选择使用、丢弃道具。新道具获得后会有红点展示,当玩家看过道具后就不再显示红点了。背包格子有上限,随着玩家等级解锁或者随着玩家购买道具、VIP 等级等解锁。
这里我用伪代码示例:
//背包 Model
public class BagModel
{
public IReadonlyList<Item> AllItems {get;}//所有的物品;
public void ReqItems();//请求所有背包数据;
public void ReqUseItem(Item item);//请求使用道具;
public void ReqDropItem(item item);//请求丢弃道具;
public void GetBagLimitCount();//获取背包上限;
}
//道具数据结构
public class Item
{
public int Type {get;}//类型
public int Count {get;}//数量
public bool IsNewItem {get;}//是否新道具
}
背包模型就很简单,只单纯地收发消息,并将服务器数据转换成客户端的数据结构就行了。
然后是 ViewModel:
//背包的 ViewModel
public class BagViewModel
{
public string Title {get;}//当前界面的标题;
public bool IsUseBtnEnable {get;}//使用按钮是否可用;
public string UseBtnLabel {get;}//使用按钮的文本;
public bool IsDropBtnEnable {get;}//丢弃按钮是否可用;
public string DropBtnLabel {get;}//丢弃按钮的文本;
public string ItemCountLabel {get;}//物品上限文本:“当前数量/上限”
public E_ItemCategoryType CurSelectType {get;}//当前选择的物品类型页签;
public IReadonlyList<Item> CurrentCategoryItems {get;}//当前类型应该展示的物品实例;
public void SetCurSelectCategoryType(E_ItemCategoryType type);//设置当前选择的页签;
public string CategoryToLabel(E_ItemCategoryType type);//将标签类型转换为显示文本;
public void OnClickUseBtn();//点击使用按钮的功能;
public void OnClickDropBtn();//点击丢弃按钮的功能;
public void SetItemSeen(Item item);//设置某个物品看过了;
}
当然,这里的 ViewModel 我没有写完整,示意一下,大家大概能知道这里面会是什么数据就行了。最后是 UI 代码,也就是 View 层:
//背包窗口 -- 视图层 View;
public class BagWindow : MonoBehaviour
{
public TextMeshProUGUI Title;//标题文本组件;
public RectTransform CatagoryContent;//物品分类页签;
public RectTransform ItemContent;//物品布局
public TextMeshProUGUI ItemCountLabel;//物品数量文本;
public Button BtnUse;//使用按钮;
public Button BtnDrop;//丢弃按钮;
public void Start()//打开页面……
{
BagViewModel vm = GetViewModel();//获取目标 VM;
//直接对各个组件进行赋值
Title.text = vm.Title;
ItemCountLabel.text = vm.ItemCountLabel;
BtnUse.SetActive(vm.IsUseBtnEnable);
BtnUse.OnClick.AddListener(vm.OnClickUseBtn);
……
}
//省略:TableList 的 Item 展示、页签的生成和绑定等;
}
从上面的代码不难发现,View 其实不与 Model 直接交互,而是和 ViewModel 交互。View 层去逻辑,甚至连多语言文本都不去拼接,按钮状态也不判断,VM 是什么就照抄什么。因此我们验收时,只要确保 ViewModel 层正确,那就认为 View 层也正确。毕竟,现在的 View 逻辑就纯粹地 “翻译”,这种写法基本不会出问题。
脏标记与刷新
在使用 MVVM 开发时,有一个问题就是 Model 和 ViewModel 大概率会常驻内存,因为一个 ViewModel 可能不止对应一个界面,和 Model 的生命周期一致更为合适。如果每次 Model 变化都要 ViewModel 刷新数据,性能上还是不友好的:因为此时对应 View 模块可能根本不存在,根本没必要刷新数值。而且从设计上,ViewModel 不应该持有 View 对象,因为这套设计就是了将 View 拆出去去耦合,能让数据本身单独跑起来。
因此这里简单的做法就是脏标记和视图层触发刷新逻辑。

这里的好处就是,如果数据没更新,那么 ViewModel 和 View 都不会变化。而 Model 层变化了,View 只能通过 Update 来刷新数据,而非触发式的。好在面板层的 Update 频率是可以控制的,不用每帧都刷新。这里有个面板变动事件,算是一个松耦合:VM 并不知道 View 存在与否,View 也不知道 Model 层具体啥时候完成了更新(假设存在多线程情况),那这里就通过事件(也可以有别的方式)让 View 层刷新界面显示。
如果面板没有打开,数据变动只会修改脏标记,这个代价就很低了。面板打开,则可以同时支持事件触发更新和轮询更新两种情况,对绝大部分视图层都够用了。
对于某些复杂页面,他一个 VM 会包含很多信息,而且有的数据更新刷新代价较大,则可以对脏标记分块,用位运算标记不同模块,只在 VM 内部处理就可以了。
什么时候构建 ViewModel?
这一套体系其实算比较重度的了,有许多功能本身就比较轻量化,用 ViewModel 有一种大炮打蚊子的感觉:
-
一个飘字提示:只有一行文本和固定的动画;
-
确认框:只有文字,固定的确认按钮,无功能;
-
物品展示控件:本身就没有逻辑,展示图片、文字、数量等,所有的逻辑都是项目公共 API;
我也不建议这些控件都建立自己的 VM,他们自己本身就已经去逻辑化了。例如物品控件展示图片,在部分项目里,已经有公开 API:支持传入一个 ItemID,返回一个 Sprite,内部已经处理好了资源加载和异常处理。那我觉得这一部分也确实不需要 VM。
还有一种情况,项目已经把部分常用组件,例如 Slider、TableList、SwitchToggle 这种通用化了,只用传递少量参数,然后再在编辑器里配置就能完成需求。那这部分 VM 化也不太需要,因为我们构建 VM 主要是为了避免调试视图逻辑;一条路就是去逻辑化,就是 ViewModel;另一种就是使用已经确认过的通用逻辑,上述说的显然属于第二种,包括 Dotween、Unity Animation 还有各种插件,都属于不需测试的可信任逻辑。
那什么时候需要构建 ViewModel ?
其实有点凭感觉,但我个人经验:当你觉得这个模块存在专用逻辑,且需要持有局部变量的时候,就可以考虑构建 ViewModel 了。
总结
总之,这套体系的核心原则是将所有视图逻辑收敛至ViewModel层,View层去逻辑化,仅忠实反映数据。好处在于:逻辑验收只需验证Model与ViewModel的数据结果,无需启动完整游戏流程;视图层仅写胶水代码,几乎无逻辑。同时通过脏标记与视图层触发刷新机制避免不必要的性能开销。该架构依赖现代引擎视图层执行的高可靠性,适合AI高频开发场景。
最后,我们需要回到我们的首要目的:
功能逻辑纯数据化 —— 所有业务逻辑收敛为可测的纯数据与纯函数,便于脱离 Unity 图形环境验收
视图表现去逻辑化 —— View 仅作为 VM 数据的翻译器,不含业务判断与状态
MVVM 是实现以上两点的重要手段,而非目的本身。当某模块已被去逻辑化或抽象为可信任的通用层时,可以直接跳过 MVVM。
更多推荐


所有评论(0)