在Unity中使用UiToolkit进行编辑器开发

UItoolkit简介

        通常Unity的UI系统可以分为四套:NGUI,UGUI,IMGUI,UItoolkit,今天就来介绍一下最新的UItoolkit

        UI Toolkit 是 Unity 提供的一个现代化的 UI 框架,用于在 Unity 编辑器和游戏中创建用户界面。它最初作为编辑器扩展工具推出,但从 Unity 2021.1 开始,它已被扩展为一个完整的 UI 系统,适用于运行时和编辑器中的用户界面开发。UI Toolkit 提供了基于 Web 的 UI 开发模型,类似于 HTML 和 CSS,使用的是一个基于树状结构的 UI 系统。

        但是今天我们只关注使用uitoolkit进行编辑器开发,实现我们自己的工作流(对于一些重复的工作,我们使用开发的编辑器,达到一键生成的效果)

Uitoolkit进行页面制作

使用UItoolkit构建页面也有三种手段:

1 Ui Builder:这可以进行可视化编辑,类似我们使用UGUI拼面板,通过Unity左上方的Window/UItoolkit/Ui Builder打开这个页面

2 编写UXML文件,类似写xml

3 使用c#

我推荐第三种,毕竟我们最熟悉,学习成本相对最低,本篇也使用第三种方式讲解

下面通过代码来展开讲解

首先要说一些我面临的环境,即我写下面的代码是为了解决什么问题

        在使用UGUI拼写面板的过程中我会重复面临这些步骤,创建一个测试场景(每个面板拥有一个测试场景,既能测试又能帮助搭建UI面板),一个面板预制体,一个和面板同名的脚本,一个静态文件夹(该文件夹存放拼面板所用的图片,且在面板中是不变得),一个动态文件夹(存放需要动态加载到面板上的图片),这样5个依次创建下来很麻烦,是重复的工作

using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UIElements;
using System.Reflection;
using System;
using System.Linq;
using System.CodeDom.Compiler;

public class AutoGenerate : EditorWindow//要生产面板我们需要继承这个类
{
    #region Component

    private VisualElement root;//用来存放面板的根,每个面板都有一个唯一的根
    private Label titleText;//标签组件,类似UGUI的Text
    private Label hintText;//用来展示提示文本
    //这两个开关来做可选项,因为5个钟测试场景和动态文件夹不一定需要
    private Toggle testScene; //测试场景是否生成 该组件类似UGUI的开关组件
    private Toggle dynamicSprite; //动态文件夹是否生成

    private Button generateBtn;//生产按钮
    private TextField nameInput;//输入框,接受一个输入

    #endregion


    #region Path//存放5个对应的路径

    private string testScenePath;
    private string scriptPath;
    private string uiPrefabPath;
    private string uiStaticSpritePath;
    private string uidynamicSpritePath;

    private string uiTemplatePath;//存放UI模版预制体的路径,其实就是一个没用img组件的Panel

    #endregion


    [MenuItem("Tools/Generate Editor Window")]//添加该特性可以在Unity中自定义打开面板的方式
    public static void ShowExample()
    {
        AutoGenerate wnd = GetWindow<AutoGenerate>();
        wnd.titleContent = new GUIContent("Auto Generate UI Window");//面板标题
    }
    //类似声明周期方法
    public void CreateGUI()
    {
        testScenePath = "Assets/TempDirectory/TestUIPanelScenes";
        scriptPath = "Assets/Scripts/Project/UI/UI_Panel";
        uiPrefabPath = "Assets/Resources/UI/UI_Panel";
        uiStaticSpritePath = "Assets/ArtRes/Sprite/UI_Panel";
        uidynamicSpritePath = "Assets/Resources/Sprites";
        uiTemplatePath = Path.Combine("Assets", "TempDirectory",
            "TestUIPanelScenes", "OnlyUITemplate",
            "CustomPanel.prefab");
        //下面这段注释就遇到一个情况:我们使用的是第三种方法,第一种使用UI Builder搭建的面板
        //可以保存为一个资源文件,我们可以加载该文件作为面板,点到为止不多讨论
        // var visualTree =
        //     AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(
        //         "Assets/ThirdPart/UIToolKitFile/UDoc/UXml/AutoGenerateUI.uxml");
        //  rootElement = visualTree.CloneTree();

        
        root = rootVisualElement;//该属性即代表该面板的根,拿过来存起来备用

        //下列是为组件赋值,构造或者一些字段赋值可以为组件添加一些文字标识
        titleText = new Label("自动生成ui预制体,脚本,静态sprite文件夹,动态sprite文件夹(可选),测试场景(可选),");
        hintText = new Label();
        hintText.style.fontSize = 18;  // 设置字体大小
        hintText.style.color = Color.red;  // 设置字体颜色
        hintText.style.width = 300;
        hintText.style.height = 50;

        testScene = new Toggle("创建测试场景?");
        dynamicSprite = new Toggle("创建动态sprite文件夹?");

        generateBtn = new Button(OnGenerateButtonClick);
        generateBtn.text = "自动生成按钮";
        nameInput = new TextField("预制体名(也是脚本名)");

        //添加到面板上,添加顺序即在面板上的展示顺序
        root.Add(titleText);
        root.Add(hintText);
        root.Add(testScene);
        root.Add(dynamicSprite);
        root.Add(nameInput);
        root.Add(generateBtn);

        // nameInput = rootElement.Q<TextField>("NameInput");
    }
    //该方法放到按键的构造中,被按钮绑定
    private void OnGenerateButtonClick()
    {
        CheckDirectoryAndCreat(); //检查五个路径文件夹是否都在,没有则创建
        string tempName = nameInput.text;//获取输入值
        bool isLaw=CheckLawfulString(tempName);
        if(!isLaw)
        {
             ShowHint("非法名称");
             return;
        }

        var sceneExist = DoesFileExist(testScenePath, tempName);
        var preExist = DoesFileExist(uiPrefabPath, tempName);
        var scriptExist = DoesFileExist(scriptPath, tempName);
        if (sceneExist || preExist || scriptExist)
        {
            ShowHint("三者中至少有一个同名文件已经存在,未添加任何文件");
            return;
        }

        CreatPrefab(tempName);
        CreatCSFile(tempName);

        DirectoryDontExistAndCreat(uiStaticSpritePath + "/" + tempName);
        if (dynamicSprite.value)
        {
            DirectoryDontExistAndCreat(uidynamicSpritePath + "/" + tempName);
        }

        if (testScene.value)
        {
            CreateNewScene(testScenePath, tempName);
        }

        AssetDatabase.Refresh();//强制刷新一下资产目录
      
    }
    //检查输入的名称是否合法(比如不能是数字开头)
    private bool CheckLawfulString(string className)
    {
         if (string.IsNullOrWhiteSpace(className))
        {
            return false;
        }

        using (CodeDomProvider provider = CodeDomProvider.CreateProvider("CSharp"))
        {
            // 使用 CodeDomProvider 来检查标识符是否合法
            bool isValidIdentifier = provider.IsValidIdentifier(className);

            if (isValidIdentifier && !char.IsLower(className[0])) return true;
            else return false;
        }
    }

    private void CheckDirectoryAndCreat()
    {
        DirectoryDontExistAndCreat(uiPrefabPath);
        DirectoryDontExistAndCreat(testScenePath);
        DirectoryDontExistAndCreat(scriptPath);
        DirectoryDontExistAndCreat(uidynamicSpritePath);
        DirectoryDontExistAndCreat(uiStaticSpritePath);
    }

    private void DirectoryDontExistAndCreat(string directoryPath)
    {
        if (!Directory.Exists(directoryPath))
        {
            Directory.CreateDirectory(directoryPath);
        }
    }

    private void CreatCSFile(string csFileName)
    {
        string scriptContent = GetScriptTemplate(csFileName);
        File.WriteAllText(Path.Combine(scriptPath, csFileName + ".cs"), scriptContent);
    }

    private void CreatPrefab(string preName)
    {
        //Selection.activeGameObject;
        GameObject selectedObjectRes = AssetDatabase.LoadAssetAtPath<GameObject>(uiTemplatePath);

        if (selectedObjectRes == null)
        {
            ShowHint("面板模版预制体不存在");
            return;
        }

        GameObject selectedObject = (GameObject)PrefabUtility.InstantiatePrefab(selectedObjectRes);
        PrefabUtility.UnpackPrefabInstance(selectedObject, PrefabUnpackMode.Completely,
            InteractionMode.AutomatedAction); //完全解包且不提示
        //解包属性会出问题,重置一下
        selectedObject.transform.localPosition = Vector3.zero;
        selectedObject.transform.localScale = Vector3.one;
        selectedObject.name = preName;
        var generatePre =
            PrefabUtility.SaveAsPrefabAsset(selectedObject, Path.Combine(uiPrefabPath, preName + ".prefab"));
    }

    private string GetScriptTemplate(string scriptName)
    {
        return $@"
using UnityEngine;

public class {scriptName} : MonoBehaviour
{{
    public void Init()
   {{
   }}
    void Start()
    {{
       
    }}

}}";
    }

    /// <summary>
    /// 提示文本,且一定时间自动清空
    /// </summary>
    /// <param name="mes"></param>
    private void ShowHint(string mes)
    {
        hintText.text = mes;
        hintText.style.opacity = 1f; // 确保提示文本完全可见
        float timer = (float)EditorApplication.timeSinceStartup + 2;
        EditorApplication.CallbackFunction updateCallback = null;
        updateCallback= () =>
        {
            if (EditorApplication.timeSinceStartup>=timer)
            {
                hintText.text = string.Empty;
                EditorApplication.update -= updateCallback;
            }
        };
        EditorApplication.update += updateCallback;
    }

    /// <summary>
    /// 检查文件是否存在
    /// </summary>
    /// <param name="path"></param>
    /// <param name="fileName"></param>
    /// <returns></returns>
    private bool DoesFileExist(string path, string fileName)
    {
        string fullPath = path;
        string[] files = Directory.GetFiles(fullPath, "*.*", SearchOption.TopDirectoryOnly); //不递归
        foreach (var file in files)
        {
            //无拓展名
            if (Path.GetFileNameWithoutExtension(file) == fileName)
            {
                return true;
            }
        }

        return false;
    }

    /// <summary>
    /// 创建场景
    /// </summary>
    /// <param name="scenePath"></param>
    /// <param name="sceneName"></param>
    private void CreateNewScene(string scenePath, string sceneName)
    {
        //TODO 将预制体添加到场景未实现
        var newScene = EditorSceneManager.NewScene(NewSceneSetup.DefaultGameObjects, NewSceneMode.Single);

        //如果你想在场景中再添加一些物体可以想下面这样,但是我上面创建的场景默认已经有light了
        // GameObject lightGameObject = new GameObject("Directional Light");
        // Light lightComp = lightGameObject.AddComponent<Light>();
        // lightComp.type = LightType.Directional;
        // lightComp.transform.rotation = Quaternion.Euler(50, -30, 0);
        //
        // GameObject ground = GameObject.CreatePrimitive(PrimitiveType.Plane);
        // ground.transform.position = Vector3.zero;

        // 加载预制体资源,就是一个Canvas
        GameObject prefab =
            AssetDatabase.LoadAssetAtPath<GameObject>(Path.Combine("Assets", "Resources", "UI", "Base",
                "Canvas.prefab"));
       
        if (prefab != null)
        {
            // 实例化预制体并添加到场景中
            GameObject prefabInstance = (GameObject)PrefabUtility.InstantiatePrefab(prefab);
         
            SceneManager.MoveGameObjectToScene(prefabInstance, newScene);
         
        }
        else
        {
            Debug.LogError("预制体未找到,请检查路径是否正确!");
        }

        EditorSceneManager.SaveScene(newScene, scenePath + "/" + sceneName + ".unity", true);
    }

    //这里我想将创建的脚本也被添加到UI预制体上,但暂时没实现
    private void AttachScriptToGameObject(string className, GameObject pre)
    {
        // 获取当前的Assembly
        var assembly = Assembly.Load("Assembly-CSharp");

        // 使用反射获取新脚本的类型
        Type scriptType = assembly.GetTypes().FirstOrDefault(t => t.Name == className);

        if (scriptType != null)
        {
            // 查找目标游戏对象(假设场景中有一个名为 "TargetObject" 的对象)

            if (pre != null)
            {
                // 添加脚本到对象上
                pre.AddComponent(scriptType);
                ShowHint($"{className} script attached to {pre.name}");
            }
            else
            {
                Debug.LogError("Target object not found in the scene.");
            }
        }
        else
        {
            Debug.LogError($"Script type {className} not found.");
        }
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值