Unity 自动生成UI绑定+MVVM 架构模板

环境检查:安装 UniTask


通过 Git URL 一键拉取 (最推荐)

这是最官方、最快捷的方式,直接从 GitHub 把源码作为 Package 挂载进项目,保证你的 Assets 文件夹干干净净!

操作步骤:

  1. 打开你的 Unity 工程。

  2. 在顶部菜单栏点击 Window \\rightarrow Package Manager

  3. 在弹出的窗口左上角,点击那个小小的 + 号图标。

  4. 在下拉菜单中选择 Add package from git URL...

  5. 在输入框里,精准地复制粘贴这串官方地址(注意,一字不差):

    Plaintext

    复制代码
    https://github.com/Cysharp/UniTask.git?path=src/UniTask/Assets/Plugins/UniTask
  6. 点击 Add,等进度条跑完。搞定!🎉

(⚠️ 提示:这种方式需要你的电脑上安装了 Git 环境)

第一部分:核心运行时底座 (BindableProperty.cs)

职责:提供轻量级响应式数据流和生命周期管理扩展。

cs 复制代码
using System;
using System.Collections.Generic;

/// <summary>
/// 🚀 轻量级响应式属性 - 严格 MVVM 数据层
/// </summary>
public class BindableProperty<T>
{
    private T _value;
    private Action<T> _onValueChanged;

    public T Value
    {
        get => _value;
        set
        {
            if (EqualityComparer<T>.Default.Equals(_value, value)) return;
            _value = value;
            _onValueChanged?.Invoke(_value);
        }
    }

    public BindableProperty(T initialValue = default)
    {
        _value = initialValue;
    }

    public IDisposable Subscribe(Action<T> action)
    {
        _onValueChanged += action;
        action?.Invoke(_value); 
        return new Unsubscriber(() => _onValueChanged -= action);
    }

    private class Unsubscriber : IDisposable
    {
        private Action _unsubscribeAction;
        public Unsubscriber(Action unsubscribeAction) => _unsubscribeAction = unsubscribeAction;
        public void Dispose()
        {
            _unsubscribeAction?.Invoke();
            _unsubscribeAction = null;
        }
    }
}

public static class BindablePropertyExtensions
{
    /// <summary>
    /// 将订阅加入清理桶,随 View 销毁自动释放
    /// </summary>
    public static void AddTo(this IDisposable disposable, ICollection<IDisposable> disposables)
    {
        if (disposable != null && disposables != null) disposables.Add(disposable);
    }
}

第二部分:全自动生成引擎 (UIOneKeyScaffold.cs)

职责 :严格执行 ViewViewModel 后缀命名,扫描规约节点,生成三件套,并自动挂载。

cs 复制代码
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using UnityEditor;
using UnityEngine;

public class UIOneKeyScaffold
{
    // 配置生成路径(可根据项目需求修改)
    private const string SCRIPT_GEN_PATH = "Assets/Scripts/UI/Generated/";

    [MenuItem("GameObject/大厂工具链/🚀 生成严格 MVVM 架构模板", false, -11)]
    [MenuItem("Assets/大厂工具链/🚀 生成严格 MVVM 架构模板", false, 103)]
    public static void GenerateStrictMVVM()
    {
        GameObject selectedObj = Selection.activeGameObject;
        if (selectedObj == null)
        {
            Debug.LogError("❌ 请先选中一个 UI 预制体或节点!");
            return;
        }

        // 1. 提取核心名称 (例如 LoginPanel -> Login)
        string rawName = selectedObj.name.Replace("(Clone)", "").Trim();
        string coreName = rawName.Replace("View", "").Replace("Panel", "").Replace("UI", "");
        
        string viewName = coreName + "View";
        string viewModelName = coreName + "ViewModel";

        // 2. 扫描符合规约的节点 (btn_, txt_, img_, input_, obj_)
        var bindInfos = new List<ComponentBindInfo>();
        CollectBindNodes(selectedObj.transform, "", bindInfos);

        // 3. 准备文件夹
        string fullPath = Path.Combine(Application.dataPath, SCRIPT_GEN_PATH.Replace("Assets/", ""));
        if (!Directory.Exists(fullPath)) Directory.CreateDirectory(fullPath);

        // 4. 生成三件套
        GenerateViewModelScript(viewModelName);
        GenerateViewScript(viewName, viewModelName);
        GenerateViewBindScript(viewName, bindInfos);

        // 5. 记录挂载信息 (跨编译周期)
        string assetPath = PrefabUtility.GetPrefabAssetPathOfNearestInstanceRoot(selectedObj);
        bool isPrefab = !string.IsNullOrEmpty(assetPath);
        EditorPrefs.SetString("AutoAttach_ClassName", viewName);
        EditorPrefs.SetBool("AutoAttach_IsPrefab", isPrefab);
        EditorPrefs.SetString("AutoAttach_TargetPath", isPrefab ? assetPath : selectedObj.name);

        Debug.Log($"⏳ [MVVM 自动化]:正在为 {coreName} 构建工程,请稍后...");
        AssetDatabase.Refresh(); 
    }

    private static void GenerateViewModelScript(string vmName)
    {
        string filePath = Path.Combine(Application.dataPath, SCRIPT_GEN_PATH.Replace("Assets/", ""), $"{vmName}.cs");
        if (File.Exists(filePath)) return;

        StringBuilder sb = new StringBuilder();
        sb.AppendLine("using System;");
        sb.AppendLine("using System.Threading;");
        sb.AppendLine("using Cysharp.Threading.Tasks;");
        sb.AppendLine();
        sb.AppendLine($"public class {vmName}");
        sb.AppendLine("{");
        sb.AppendLine("    // 💡 ViewModel 严禁引用 UnityEngine.UI");
        sb.AppendLine("    public BindableProperty<string> HintMessage = new BindableProperty<string>(\"Ready\");");
        sb.AppendLine();
        sb.AppendLine("    public async UniTask DoActionAsync(CancellationToken ct)");
        sb.AppendLine("    {");
        sb.AppendLine("        HintMessage.Value = \"Executing...\";");
        sb.AppendLine("        await UniTask.Delay(1000, cancellationToken: ct);");
        sb.AppendLine("        HintMessage.Value = \"Success!\";");
        sb.AppendLine("    }");
        sb.AppendLine("}");
        File.WriteAllText(filePath, sb.ToString(), Encoding.UTF8);
    }

    private static void GenerateViewScript(string vName, string vmName)
    {
        string filePath = Path.Combine(Application.dataPath, SCRIPT_GEN_PATH.Replace("Assets/", ""), $"{vName}.cs");
        if (File.Exists(filePath)) return;

        StringBuilder sb = new StringBuilder();
        sb.AppendLine("using UnityEngine;");
        sb.AppendLine("using UnityEngine.UI;");
        sb.AppendLine("using Cysharp.Threading.Tasks;");
        sb.AppendLine();
        sb.AppendLine($"public partial class {vName} : MonoBehaviour");
        sb.AppendLine("{");
        sb.AppendLine($"    private {vmName} _viewModel;");
        sb.AppendLine();
        sb.AppendLine("    private void Awake()");
        sb.AppendLine("    {");
        sb.AppendLine($"        _viewModel = new {vmName}();");
        sb.AppendLine("        AutoBind();");
        sb.AppendLine("        BindViewModel();");
        sb.AppendLine("    }");
        sb.AppendLine();
        sb.AppendLine("    private void BindViewModel()");
        sb.AppendLine("    {");
        sb.AppendLine("        // 绑定数据流示例");
        sb.AppendLine("        // _viewModel.HintMessage.Subscribe(msg => { /* txt_hint.text = msg; */ }).AddTo(_disposables);");
        sb.AppendLine("    }");
        sb.AppendLine("}");
        File.WriteAllText(filePath, sb.ToString(), Encoding.UTF8);
    }

    private static void GenerateViewBindScript(string vName, List<ComponentBindInfo> infos)
    {
        string filePath = Path.Combine(Application.dataPath, SCRIPT_GEN_PATH.Replace("Assets/", ""), $"{vName}.Bind.cs");
        StringBuilder sb = new StringBuilder();
        sb.AppendLine("// 🚀 自动生成 - 请勿手动修改");
        sb.AppendLine("using UnityEngine;");
        sb.AppendLine("using UnityEngine.UI;");
        sb.AppendLine("using System.Collections.Generic;");
        sb.AppendLine("using System;");
        sb.AppendLine();
        sb.AppendLine($"public partial class {vName}");
        sb.AppendLine("{");
        foreach (var info in infos) sb.AppendLine($"    private {info.TypeName} {info.FieldName};");
        sb.AppendLine();
        sb.AppendLine("    private List<IDisposable> _disposables = new List<IDisposable>();");
        sb.AppendLine();
        sb.AppendLine("    public void AutoBind()");
        sb.AppendLine("    {");
        foreach (var info in infos)
        {
            if (info.TypeName == "GameObject") sb.AppendLine($"        this.{info.FieldName} = transform.Find(\"{info.Path}\").gameObject;");
            else sb.AppendLine($"        this.{info.FieldName} = transform.Find(\"{info.Path}\").GetComponent<{info.TypeName}>();");
        }
        sb.AppendLine("    }");
        sb.AppendLine();
        sb.AppendLine("    private void OnDestroy()");
        sb.AppendLine("    {");
        sb.AppendLine("        foreach (var d in _disposables) d.Dispose();");
        sb.AppendLine("        _disposables.Clear();");
        sb.AppendLine("    }");
        sb.AppendLine("}");
        File.WriteAllText(filePath, sb.ToString(), Encoding.UTF8);
    }

    [UnityEditor.Callbacks.DidReloadScripts]
    private static void OnScriptsReloaded()
    {
        string className = EditorPrefs.GetString("AutoAttach_ClassName", "");
        if (string.IsNullOrEmpty(className)) return;

        string targetPath = EditorPrefs.GetString("AutoAttach_TargetPath", "");
        bool isPrefab = EditorPrefs.GetBool("AutoAttach_IsPrefab", false);
        EditorPrefs.DeleteKey("AutoAttach_ClassName");

        Type type = GetTypeByName(className);
        if (type == null) return;

        GameObject obj = isPrefab ? AssetDatabase.LoadAssetAtPath<GameObject>(targetPath) : GameObject.Find(targetPath);
        if (obj != null && obj.GetComponent(type) == null)
        {
            obj.AddComponent(type);
            if (isPrefab) PrefabUtility.SavePrefabAsset(obj);
            Debug.Log($"✅ {className} 已成功挂载!架构已就绪。");
        }
    }

    private static void CollectBindNodes(Transform current, string path, List<ComponentBindInfo> infos)
    {
        string nodeName = current.name.ToLower();
        string relPath = string.IsNullOrEmpty(path) ? "" : path + (path == "" ? "" : "/") + current.name;

        if (nodeName.StartsWith("btn_")) AddInfo(infos, current, "Button", relPath);
        else if (nodeName.StartsWith("txt_")) AddInfo(infos, current, "Text", relPath);
        else if (nodeName.StartsWith("img_")) AddInfo(infos, current, "Image", relPath);
        else if (nodeName.StartsWith("input_")) AddInfo(infos, current, "InputField", relPath);
        else if (nodeName.StartsWith("obj_")) AddInfo(infos, current, "GameObject", relPath);

        foreach (Transform child in current)
        {
            string nextRel = string.IsNullOrEmpty(path) ? current.name : path + "/" + current.name;
            CollectBindNodes(child, nextRel, infos);
        }
    }

    private static void AddInfo(List<ComponentBindInfo> infos, Transform trans, string type, string path)
    {
        string finalPath = path.Contains("/") ? path.Substring(path.IndexOf('/') + 1) : "";
        if (string.IsNullOrEmpty(finalPath)) return;
        infos.Add(new ComponentBindInfo { FieldName = trans.name, TypeName = type, Path = finalPath });
    }

    private static Type GetTypeByName(string className)
    {
        return AppDomain.CurrentDomain.GetAssemblies()
            .SelectMany(a => a.GetTypes())
            .FirstOrDefault(t => t.Name == className);
    }

    private class ComponentBindInfo { public string FieldName; public string TypeName; public string Path; }
}

开发者使用指南(严格执行流程)

  1. 环境检查 :确认已安装 UniTask

  2. 美术规约:在 UI 层级中,需要程序控制的节点必须以前缀命名:

    • btn_Login (Button)

    • txt_Title (Text)

    • img_Avatar (Image)

    • input_Account (InputField)

    • obj_Loading (GameObject)

  3. 一键造物 :选中 UI 根节点,右键点击 🚀 生成严格 MVVM 架构模板

  4. 架构产出

    • Logic (XXXViewModel.cs) : 处理纯逻辑,数据更新通过修改 BindableProperty

    • Controller (XXXView.cs) : 处理绑定,调用 _viewModel.Execute...().Forget()

    • Auto-Bind (XXXView.Bind.cs) : 工具维护,负责 UI 引用查找和 OnDestroy 清理。

  5. 自动关联:等待 Unity 编译完成后,脚本会自动挂载到你的 UI 物体上。

示例:

cs 复制代码
// 🚀 自动生成 - 请勿手动修改
using UnityEngine;
using UnityEngine.UI;
using System.Collections.Generic;
using System;

public partial class Test1View
{
    private Image img_1;
    private Text txt_5;
    private Button btn_2;
    private Text txt_7;
    private Text txt_3;
    private Image img_6;
    private InputField input_4;

    private List<IDisposable> _disposables = new List<IDisposable>();

    public void AutoBind()
    {
        this.img_1 = transform.Find("img_1").GetComponent<Image>();
        this.txt_5 = transform.Find("img_1/txt_5").GetComponent<Text>();
        this.btn_2 = transform.Find("btn_2").GetComponent<Button>();
        this.txt_7 = transform.Find("btn_2/txt_7").GetComponent<Text>();
        this.txt_3 = transform.Find("txt_3").GetComponent<Text>();
        this.img_6 = transform.Find("txt_3/img_6").GetComponent<Image>();
        this.input_4 = transform.Find("input_4").GetComponent<InputField>();
    }

    private void OnDestroy()
    {
        foreach (var d in _disposables) d.Dispose();
        _disposables.Clear();
    }
}
cs 复制代码
using UnityEngine;
using UnityEngine.UI;
using Cysharp.Threading.Tasks;

public partial class Test1View : MonoBehaviour
{
    private Test1ViewModel _viewModel;

    private void Awake()
    {
        _viewModel = new Test1ViewModel();
        AutoBind();
        BindViewModel();
    }

    private void BindViewModel()
    {
        // 绑定数据流示例
        _viewModel.HintMessage.Subscribe(msg => { txt_3.text = msg; }).AddTo(_disposables);
    }
}
cs 复制代码
using System;
using System.Threading;
using Cysharp.Threading.Tasks;

public class Test1ViewModel
{
    // 💡 ViewModel 严禁引用 UnityEngine.UI
    public BindableProperty<string> HintMessage = new BindableProperty<string>("Ready");

    public async UniTask DoActionAsync(CancellationToken ct)
    {
        HintMessage.Value = "Executing...";
        await UniTask.Delay(1000, cancellationToken: ct);
        HintMessage.Value = "Success!";
    }
}
相关推荐
winlife_11 小时前
Unity 域重载会清空一切:Editor 工具如何让状态在重载后续命
unity·游戏引擎
小贺儿开发13 小时前
Unity3D 串口通信上位机联调系统
unity·串口·协议·数据·通信·传输·互动
垂葛酒肝汤1 天前
Unity的UI扫光效果Shader
ui·unity·游戏引擎
mxwin2 天前
Unity Shader Alpha测试 · 模板测试 · 深度测试
unity·游戏引擎
Sator12 天前
unity解决粒子与物体接触时的硬边缘问题
unity·游戏引擎
程序员JerrySUN2 天前
Jetson边缘嵌入式实战课程第三讲:L4T 与 Jetson 系统架构
linux·服务器·人工智能·安全·unity·系统架构·游戏引擎
萌萌的提莫队长2 天前
Unity HDRP 渲染管线 Camera 输出到RenderTexture没有Alpha通道
unity·游戏引擎
winlife_2 天前
Unity Editor 工具不该用反射写组件字段:SerializedObject 在 4 个场景里非用不可
unity·自动化·游戏引擎
星河耀银海2 天前
Unity C#入门:变量的定义与访问权限(public/private)
unity·c#·lucene
郝学胜-神的一滴2 天前
中级OpenGL教程 005:为球体&平面注入法线灵魂
c++·unity·图形渲染·three.js·opengl·unreal