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.");
}
}
}