Unity编辑器扩展-EditorUtility公共类(提示窗文件选择框等)

EditorUtility公共类

它是 Unity 编辑器中的一个实用工具类

提供了一系列用于编辑器脚本和自定义编辑器的实用功能

在编辑器相关处都可以使用EditorUtility公共类中的相关内容

它主要提供的是一些辅助功能,可以在编辑器拓展开发的任意地方使用

但一定注意,它属于编辑器功能,无法被打包出去,只能在Unity编辑器中使用

EditorUtility API

显示提示窗口

//EditorUtility.DisplayDialog("标题", "显示信息", "确定键名");
//注意:窗口显示会阻塞逻辑
private void OnGUI()
{
    if(GUILayout.Button("显示提示窗口"))
    {
        if(EditorUtility.DisplayDialog("测试窗口", "确定一定要做这件事情吗", "一定要做"))
        {
            Debug.Log("确定要做,在这里去处理逻辑");
        }
        else
        {
            Debug.Log("点击了叉叉,不去做");
        }
        Debug.Log("窗口显示完毕");
    }
}

显示三键提示面板

//  int EditorUtility.DisplayDialogComplex("标题", "显示信息", "按钮1名字", "按钮3名字", "按钮2名字");
//  返回值 0-按钮1按下 1-按钮3按下 2-按钮2按下
//注意:窗口显示会阻塞逻辑
private void OnGUI()
{
    if(GUILayout.Button("显示三键提示窗口"))
    {
        int result = EditorUtility.DisplayDialogComplex("三键提示", "显示信息", "选项1", "关闭", "选项2");
        switch (result)
        {
            case 0:
                Debug.Log("选项1按下了");
                break;
            case 1:
                Debug.Log("关闭键按下了");
                break;
            case 2:
                Debug.Log("选项2按下了");
                break;
            default:
                break;
        }
        Debug.Log("三键窗口显示完毕");
    }
}

进度条相关

Unity编辑器扩展-窗口扩展

特殊文件夹Editor

Editor文件夹是Unity中的特殊文件夹

所有Unity中编辑器相关的脚本都需要放置在其中

用了Unity编辑器相关命名空间的脚本最终是不能被打包出去

需要把这些脚本放置到Editor文件夹中,避免打包时报错

菜单栏扩展

在Unity菜单栏中添加自定义页签

命名空间:UnityEditor

特性:MenuItem

用法:

在静态函数前加上[MenuItem(“页签/一级选项/二级选项/….”)]

注意:

  1. 不需要继承mono
  2. 斜杠必须是/
[MenuItem("Unity编辑器拓展/Lesson1/TestFun %#&A")]
private static void TestFun()
{
    Debug.Log("TestFun");
}

在Hierarchy窗口中添加自定义页签

[MenuItem("GameObject/Lesson1/TestFun2")]
private static void TestFun2()
{
    Debug.Log("TestFun2");
}

在Project窗口中添加自定义页签

[MenuItem("Assets/Lesson1/TestFun3")]
private static void TestFun3()
{
    Debug.Log("TestFun3");
}

在Inspector为脚本右键添加菜单

Unity编辑器扩展-Inspector窗口扩展

Inspector窗口扩展

我们可以完全自定义 某一个脚本 在Inspector窗口的显示

SerializedObject和SerializedProperty的作用

Unity通过反射将类中的成员显示在Inspector窗口中

SerializedObject 和 SerializedProperty用于在 Unity 编辑器中操作和修改序列化对象的属性。

它们通常在自定义编辑器中使用,以创建更灵活、可定制的属性面板

自定义 脚本在Inspector窗口中显示的内容

  1. 需要单独写一个脚本,并且脚本继承自Editor
  2. 在该脚本前添加特性**[CustomEditor(想要自定义脚本类名的Type)]**
  3. 声明对应SerializedProperty序列化属性进行关联
  4. 重写OnInspectorGUI函数
public class Test : MonoBehaviour
{
    public int atk;
    public float def;
    public GameObject obj;
    public List<GameObject> objList;
}

[CustomEditor(typeof(Test))]
public class TestEditor : Editor
{
    private SerializedProperty atk;
    private SerializedProperty def;
    private SerializedProperty obj;
    private SerializedProperty objList;
    private void OnEnable()
    {
        atk = serializedObject.FindProperty("atk");
        def = serializedObject.FindProperty("def");
        obj = serializedObject.FindProperty("obj");
        objList = serializedObject.FindProperty("objList");
    }
    
    //  该函数控制了Inspector窗口中显示的内容
    //  只需要在其中重写内容便可以自定义窗口
    public override void OnInspectorGUI()
    {
        serializedObject.Update();
        EditorGUILayout.IntSlider(atk, 0, 100, "攻击力");
        def.floatValue = EditorGUILayout.FloatField("防御力", def.floatValue);
        EditorGUILayout.ObjectField(obj, new GUIContent("敌对对象"));
        EditorGUILayout.PropertyField(objList,new GUIContent("敌人对象们"));
        //自定义Inspector窗口的内容
        if (GUILayout.Button("测试自定义Inspector窗口"))
        {
            Debug.Log(target.name);
        }
        serializedObject.ApplyModifiedProperties();
    }
}
  • serializedObject.Update();更新
  • serializedObject.ApplyModifiedProperties(); 保存修改

Inspector窗口中显示数组

Unity编辑器扩展-Scene窗口扩展

Scene窗口扩展

Scene窗口更新相应函数

OnSceneGUI函数会在 选中挂在自定义脚本的对象时自动执行

选中该对象时才会执行

[CustomEditor(typeof(SceneTest))]
public class SceneTestEditor : Editor
{
    private void OnSceneGUI()
    {
        Debug.Log("Scene窗口扩展相关逻辑")
    }
}
public class SceneTest : MonoBehaviour{}

自定义窗口中监听Scene窗口更新相应函数

//SceneView.duringSceneGUI += 事件函数;
public class void TestWindow : EditorWindow
{
    [MenuItem("/Unity编辑器扩展/Test/打卡窗口")]
    private static void OpenWindow()
    {
        GetWindow<TestWindow>().Show();
    }  
    private void OnEnable()
    {
        SceneView.duringSceneGUI += (view)=>
        {
            Debug.Log("自定义窗口扩展Scene");
        };
    }
}

Handles公共类

Handles类提供了很多API

让我们可以在Scene窗口中绘制我们的自定义内容

它和GUI、EditorGUI类似,只不过它专门提供给Scene窗口使用

Handles中的颜色控制

Handles.color = new Color(0, 1, 1, 0.3f);

Handles中的文本控件

ExcelDataReader读取Excel

读取Excel文件

using (FileStream fs = File.Open(Application.dataPath + "/ArtRes/Excel/PlayerInfo.xlsx", FileMode.Open, FileAccess.Read ))
{
    //通过文件流获取Excel数据
    IExcelDataReader excelReader = ExcelReaderFactory.CreateOpenXmlReader(fs);
    //将excel表中的数据转换为DataSet数据类型 方便我们 获取其中的内容
    DataSet result = excelReader.AsDataSet();
    //得到Excel文件中的所有表信息
    for (int i = 0; i < result.Tables.Count; i++)
    {
        Debug.Log("表名:" + result.Tables[i].TableName);
        Debug.Log("行数:" + result.Tables[i].Rows.Count);
        Debug.Log("列数:" + result.Tables[i].Columns.Count);
    }
    fs.Close();
}

获取Excel表中单元格的信息

DataSet result = excelReader.AsDataSet();
for (int i = 0; i < result.Tables.Count; i++)
{
    //得到其中一张表的具体数据
    DataTable table = result.Tables[i];
    //得到其中一行的数据
    DataRow row = table.Rows[0];
    //得到行中某一列的信息
    Debug.Log(row[1].ToString());
}

Excel数据持久化类

using Excel;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Data;
using System.IO;
using System.Text;
using UnityEditor;
using UnityEngine;

public class ExcelTool
{
    /// <summary>
    /// excel文件存放的路径
    /// </summary>
    public static string EXCEL_PATH = Application.dataPath + "/ArtRes/Excel/";

    /// <summary>
    /// 数据结构类脚本存储位置路径
    /// </summary>
    public static string DATA_CLASS_PATH = Application.dataPath + "/Scripts/ExcelData/DataClass/";

    /// <summary>
    /// 容器类脚本存储位置路径
    /// </summary>
    public static string DATA_CONTAINER_PATH = Application.dataPath + "/Scripts/ExcelData/Container/";

    /// <summary>
    /// 2进制数据存储位置路径
    /// </summary>
    public static string DATA_BINARY_PATH = Application.streamingAssetsPath + "/Binary/";

    /// <summary>
    /// 真正内容开始的行号
    /// </summary>
    public static int BEGIN_INDEX = 4;

    [MenuItem("GameTool/GenerateExcel")]
    private static void GenerateExcelInfo()
    {
        //记在指定路径中的所有Excel文件 用于生成对应的3个文件
        DirectoryInfo dInfo = Directory.CreateDirectory(EXCEL_PATH);
        //得到指定路径中的所有文件信息 相当于就是得到所有的Excel表
        FileInfo[] files = dInfo.GetFiles();
        //数据表容器
        DataTableCollection tableConllection;
        for (int i = 0; i < files.Length; i++)
        {
            //如果不是excel文件就不要处理了
            if (files[i].Extension != ".xlsx" &&
                files[i].Extension != ".xls")
                continue;
            //打开一个Excel文件得到其中的所有表的数据
            using (FileStream fs = files[i].Open(FileMode.Open, FileAccess.Read))
            {
                IExcelDataReader excelReader = ExcelReaderFactory.CreateOpenXmlReader(fs);
                tableConllection = excelReader.AsDataSet().Tables;
                fs.Close();
            }

            //遍历文件中的所有表的信息
            foreach (DataTable table in tableConllection)
            {
                Debug.Log(table.TableName);
                //生成数据结构类
                GenerateExcelDataClass(table);
                //生成容器类
                GenerateExcelContainer(table);
                //生成2进制数据
                GenerateExcelBinary(table);
            }

        }
    }

    /// <summary>
    /// 生成Excel表对应的数据结构类
    /// </summary>
    /// <param name="table"></param>
    private static void GenerateExcelDataClass(DataTable table)
    {
        //字段名行
        DataRow rowName = GetVariableNameRow(table);
        //字段类型行
        DataRow rowType = GetVariableTypeRow(table);

        //判断路径是否存在 没有的话 就创建文件夹
        if (!Directory.Exists(DATA_CLASS_PATH))
            Directory.CreateDirectory(DATA_CLASS_PATH);
        //如果我们要生成对应的数据结构类脚本 其实就是通过代码进行字符串拼接 然后存进文件就行了
        string str = "public class " + table.TableName + "\n{\n";

        //变量进行字符串拼接
        for (int i = 0; i < table.Columns.Count; i++)
        {
            str += "    public " + rowType[i].ToString() + " " + rowName[i].ToString() + ";\n";
        }

        str += "}";

        //把拼接好的字符串存到指定文件中去
        File.WriteAllText(DATA_CLASS_PATH + table.TableName + ".cs", str);

        //刷新Project窗口
        AssetDatabase.Refresh();
    }

    /// <summary>
    /// 生成Excel表对应的数据容器类
    /// </summary>
    /// <param name="table"></param>
    private static void GenerateExcelContainer(DataTable table)
    {
        //得到主键索引
        int keyIndex = GetKeyIndex(table);
        //得到字段类型行
        DataRow rowType = GetVariableTypeRow(table);
        //没有路径创建路径
        if (!Directory.Exists(DATA_CONTAINER_PATH))
            Directory.CreateDirectory(DATA_CONTAINER_PATH);

        string str = "using System.Collections.Generic;\n";

        str += "public class " + table.TableName + "Container" + "\n{\n";

        str += "    ";
        str += "public Dictionary<" + rowType[keyIndex].ToString() + ", " + table.TableName + ">";
        str += "dataDic = new " + "Dictionary<" + rowType[keyIndex].ToString() + ", " + table.TableName + ">();\n";

        str += "}";

        File.WriteAllText(DATA_CONTAINER_PATH + table.TableName + "Container.cs", str);

        //刷新Project窗口
        AssetDatabase.Refresh();
    }


    /// <summary>
    /// 生成excel2进制数据
    /// </summary>
    /// <param name="table"></param>
    private static void GenerateExcelBinary(DataTable table)
    {
        //没有路径创建路径
        if (!Directory.Exists(DATA_BINARY_PATH))
            Directory.CreateDirectory(DATA_BINARY_PATH);

        //创建一个2进制文件进行写入
        using (FileStream fs = new FileStream(DATA_BINARY_PATH + table.TableName + ".tang", FileMode.OpenOrCreate, FileAccess.Write))
        {
            //存储具体的excel对应的2进制信息
            //1.先要存储我们需要写多少行的数据 方便我们读取
            //-4的原因是因为 前面4行是配置规则 并不是我们需要记录的数据内容
            fs.Write(BitConverter.GetBytes(table.Rows.Count - 4), 0, 4);
            //2.存储主键的变量名
            string keyName = GetVariableNameRow(table)[GetKeyIndex(table)].ToString();
            byte[] bytes = Encoding.UTF8.GetBytes(keyName);
            //存储字符串字节数组的长度
            fs.Write(BitConverter.GetBytes(bytes.Length), 0, 4);
            //存储字符串字节数组
            fs.Write(bytes, 0, bytes.Length);

            //遍历所有内容的行 进行2进制的写入
            DataRow row;
            //得到类型行 根据类型来决定应该如何写入数据
            DataRow rowType = GetVariableTypeRow(table);
            for (int i = BEGIN_INDEX; i < table.Rows.Count; i++)
            {
                //得到一行的数据
                row = table.Rows[i];
                for (int j = 0; j < table.Columns.Count; j++)
                {
                    switch (rowType[j].ToString())
                    {
                        case "int":
                            fs.Write(BitConverter.GetBytes(int.Parse(row[j].ToString())), 0, 4);
                            break;
                        case "float":
                            fs.Write(BitConverter.GetBytes(float.Parse(row[j].ToString())), 0, 4);
                            break;
                        case "bool":
                            fs.Write(BitConverter.GetBytes(bool.Parse(row[j].ToString())), 0, 1);
                            break;
                        case "string":
                            bytes = Encoding.UTF8.GetBytes(row[j].ToString());
                            //写入字符串字节数组的长度
                            fs.Write(BitConverter.GetBytes(bytes.Length), 0, 4);
                            //写入字符串字节数组
                            fs.Write(bytes, 0, bytes.Length);
                            break;
                    }
                }
            }

            fs.Close();
        }

        AssetDatabase.Refresh();
    }

    /// <summary>
    /// 获取变量名所在行
    /// </summary>
    /// <param name="table"></param>
    /// <returns></returns>
    private static DataRow GetVariableNameRow(DataTable table)
    {
        return table.Rows[0];
    }

    /// <summary>
    /// 获取变量类型所在行
    /// </summary>
    /// <param name="table"></param>
    /// <returns></returns>
    private static DataRow GetVariableTypeRow(DataTable table)
    {
        return table.Rows[1];
    }

    
    /// <summary>
    /// 获取主键索引
    /// </summary>
    /// <param name="table"></param>
    /// <returns></returns>
    private static int GetKeyIndex(DataTable table)
    {
        DataRow row = table.Rows[2];
        for (int i = 0; i < table.Columns.Count; i++)
        {
            if (row[i].ToString() == "key")
                return i;
        }
        return 0;
    }
}

基础数据类型转字节数组

变量的本质

  • 变量的本质是2进制
  • 在内存中都以字节的形式存储着
  • 1byte = 8bit
  • 1bit(位)不是0就是1
  • 通过sizeof方法可以看到常用变量类型占用的字节空间长度
print("sbyte" + sizeof(sbyte) + "字节");
print("int" + sizeof(int) + "字节");
print("short" + sizeof(short) + "字节");
print("long" + sizeof(long) + "字节");

2进制文件读写的本质

它就是通过将各类型变量转换为字节数组

将字节数组直接存储到文件中

一般人是看不懂存储的数据的

不仅可以节约存储空间,提升效率

还可以提升安全性

而且在网络通信中我们直接传输的数据也是字节数据(2进制数据)

各类型数据和字节数据相互转换

将各类型转字节

int a = 256;
byte[] bytes = BitConverter.GetBytes(a);

字节数组转各类型

int i = BitConverter.ToInt32(bytes, 0);

标准编码格式

编码是用预先规定的方法将文字、数字或其它对象编成数码,或将信息、数据转换成规定的电脉冲信号。

二进制序列化

基础类型(非字符串)

关键类:BitConverter

命名空间:System

主要作用:除字符串的其他常用类型和字节数组互相转换

非字符串类型转二进制

byte[] bytes = BitConverter.GetBytes(1);

二进制转非字符串类型

//int类型
int i = BitConverter.ToInt32(bytes);
//重载 从数组的索引读取
i = BitConverter.ToInt32(bytes, index);

字符串

关键类:Encoding

命名空间:System.Text

主要作用:将字符串类型和字节数组相互转换,并且决定转换时使用的字符编码类型,网络通信时建议使用UTF-8

字符串转二进制

byte[] bytes = Encoding.UTF8.GetBytes("字符串转字节数组");

二进制转字符串

string str = Encoding.UTF8.GetString(bytes);
//重载 指定位置和长度
str = Encoding.UTF8.GetString(bytes, 0, bytes.Length);

类对象

自定义规则转换

通过BitConverterEncoding进行转换

C#文件操作

文件操作File类的常用内容

判断文件是否存在

if(File.Exists(Application.dataPath + "/UnityTeach.tang"))
{
    print("文件存在");
}
else
{
    print("文件不存在");
}

创建文件

//参数一:路径
//参数二:缓存大小
//参数三:描述如何创建或覆盖该文件(不常用)
    //  Asynchronous 可用于异步读写
    //  DeleteOnClose 不在使用时,自动删除
    //  Encrypted 加密
    //  None 不应用其它选项
    //  RandomAccess 随机访问文件
    //  SequentialScan 从头到尾顺序访问文件
    //  WriteThrough 通过中间缓存直接写入磁盘
FileStream fs = File.Create(Application.dataPath + "/Test.test");
//通过File.Open创建
//参数一:路径
//参数二:打开模式
FileStream fs2 = File.Open(Application.dataPath + "/Test.test"", FileMode.Create);

写入文件

新输入系统Input System

InputSystem是什么

InputSystem是Unity提供的一种新的输入系统

它相对于老的输入系统 扩展性和可自定义性更好

需要Unity2019.4以上版本 .Net 4 runtime

新老输入系统的区别

老输入系统需要我们自己写各种检测代码来判断输入并处理逻辑

新输入系统 不仅可以像老输入系统一样使用还添加了配置输入的概念

新输入系统将输入操作进行封装,让我们可以在Unity内进行输入配置文件编辑

我们不需要写代码来判断设备输入,只需要把工作重心放在逻辑处理上

导入InputSystem

InputSystem包导入

packageManager中导入Input System

选择开启哪一种输入模式

使用InputSystem默认会关闭老输入系统

选择InputSystem和 老InputManager的启用情况

File——>Build Setting——>Player Setting——>Other——>Active Input Handling

可以同时启用也可以只启用其中之一,每次启用后会重启Unity

代码控制 输入(不常用)

设备类

新输入系统 提供了对应的输入设备类 帮助我们对某一种设备进行输入检测

//键盘
Keyboard keyBoard = Keyboard.current;
//鼠标
Mouse mouse = Mouse.current;
//触摸屏
Touchscreen ts = Touchscreen.current;
//手柄
Gamepad gamePad = Gamepad.current;

检测键盘输入

//按下
if(Keyboard.current.enterKey.wasPressedThisFrame)
{
    Debug.log("检测当前帧 是否有 按下 操作");
}
//抬起
if(Keyboard.current.dKey.wasReleasedThisFrame)
{

}
//长按
if(Keyboard.current.spaceKey.isPressed)
{

}
//通过事件监听按键按下
keyBoard.onTextInput += (c) =>
{
    print("通过lambda表达式" + c);
};

检测鼠标输入

鼠标按键

Unity中实现MVC

框架基类

View层

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;

public abstract class BaseView : MonoBehaviour
{
    /// <summary>
    /// View更新方法
    /// </summary>
    /// <param name="model"></param>
    public abstract void UpdateView<T>(T model) where T : BaseModel<T>, new();
}

Controller层

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public abstract class BaseController<T> : Single<T> where T : BaseController<T>,new()
{
    /// <summary>
    /// 绑定model和view
    /// </summary>
    /// <param name="model"></param>
    /// <param name="view"></param>
    public void Band<K>(K model, BaseView view) where K : BaseModel<K>, new()
    {
        model.AddUpdateEventListener(value => view.UpdateView(value));
    }
    protected BaseController() { }
}

Model层

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;

public class BaseModel<T> : Single<T> where T : BaseModel<T>, new()
{
    protected BaseModel() { }

    private UnityAction<T> UpdateEvent;

    /// <summary>
    /// 更新Model数据
    /// </summary>
    public virtual void UpdateModel()
    {
        UpdateEvent?.Invoke(this as T);
    }

    /// <summary>
    /// 添加Model更新监听
    /// </summary>
    /// <param name="callback"></param>
    public void AddUpdateEventListener(UnityAction<T> callback) 
    {
        UpdateEvent -= callback;
        UpdateEvent += callback;
    }

    /// <summary>
    /// 移除Model更新监听
    /// </summary>
    /// <param name="callback"></param>
    public void RemoveUpdateEventlListener(UnityAction<T> callback) 
    {
        UpdateEvent -= callback;
    }
}

单例基类

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

/// <summary>
/// 单例基类(懒加载)
/// </summary>
/// <typeparam name="T"></typeparam>
public class Single<T> where T : new()
{
    private static readonly Lazy<T> instance = new(() => new T());
    public static T Instance => instance.Value;
    protected Single() { }
}

使用MVC实现对话框

DialogueModel

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;

public class DialogueModel : BaseModel<DialogueModel>
{

    private DialogueData[] dialogueDatas;//对话数据
    private int index;//对话索引
    public bool IsLastDialogue => dialogueDatas?.Length - 1 == index;//是否最后一句话
    private readonly Dictionary<string, Sprite> avatarDict = new();//头像字典
    private string AvatarName => dialogueDatas[index].avatar == string.Empty ? dialogueDatas[index].name : dialogueDatas[index].avatar;//如果没有指定头像 使用name作为头像
    public string[] Tags => dialogueDatas[index].tag.Split('|');//字符串拆分Tag
    public string Name => dialogueDatas[index].name; //对话框 显示名字
    public Sprite Avatar => avatarDict[AvatarName];//对话框头像
    public string Value => dialogueDatas[index].value; //对话内容
    public override void UpdateModel()
    {
        //更新时加载头像 字典中有则不用加载
        if (avatarDict.ContainsKey(AvatarName))
        {
            base.UpdateModel();
        }
        else
        {
            //异步加载头像
            ResourcesManager.Instance.LoadAsync<Sprite>("Sprite/Avatar/" + AvatarName, (obj) =>
            {
                if (obj != null)
                {
                    avatarDict.Add(AvatarName, obj);
                }
                base.UpdateModel();
            });
        }
    }
    /// <summary>
    /// 加载对话
    /// </summary>
    /// <param name="dialogueID">对话id</param>
    /// <param name="callback">加载完成后调用的方法</param>
    public void LoadDialogue(string dialogueID, UnityAction callback = null)
    {
        ResourcesManager.Instance.LoadAsync<DialogueSO>("SO/" + dialogueID, (data) =>
        {
            if (data == null) return;
            dialogueDatas = data.datas;
            index = 0;
            callback?.Invoke();
            UpdateModel();
        });
    }
    /// <summary>
    /// 下一句话
    /// </summary>
    public void NextDialogue()
    {
        if (!IsLastDialogue) index++;
        UpdateModel();
    }
}

DialogueController

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class DialogueController : BaseController<DialogueController>
{

    private DialogueView view;

    public DialogueController()
    {
        //测试:注册切换对话Tag事件
        EventManager.Instance.AddEventListener<string>(E_EventType.DialogueTagEvent, str =>
        {
            var strs = str.Split(':');
            if (strs[0] == "Jump")
            {
                Show(strs[1]);
            }
        });
    }

    /// <summary>
    /// 显示对话
    /// </summary>
    /// <param name="id">对话id</param>
    public void Show(string id)
    {
        if (view == null)
        {
            //加载对话面板
            ResourcesManager.Instance.LoadAsync<GameObject>("UI/DialoguePanel", (obj) =>
            {
                if (obj == null) return;
                if (GameObject.Instantiate(obj, GameObject.Find("Canvas").transform, false).TryGetComponent(out view))
                {
                    Band(DialogueModel.Instance, view);
                    DialogueModel.Instance.LoadDialogue(id, () =>
                    {
                        view.gameObject.SetActive(true);
                    });
                }
            });
        }
        else
        {
            DialogueModel.Instance.LoadDialogue(id, () =>
             {
                 view.gameObject.SetActive(true);
             });
        }
    }

    /// <summary>
    /// 隐藏面板
    /// </summary>
    public void Hide()
    {
        view.gameObject.SetActive(false);
    }

    /// <summary>
    /// 下一句对话
    /// </summary>
    public void Next()
    {
        if (!view.typeWriter.IsTextShowDone) 
        {
            view.typeWriter.SkipWriteText();
            return;
        }
        //分发Tag事件
        foreach (var tagValue in DialogueModel.Instance.Tags)
        {
            EventManager.Instance.EventTrigger(E_EventType.DialogueTagEvent, tagValue);
        }
        //如果是最后一句话 隐藏面板
        if (DialogueModel.Instance.IsLastDialogue)
        {
            Hide();
            return;
        }
        //加载下一句话
        DialogueModel.Instance.NextDialogue();
    }
}

DialogueView

using System.Collections;
using UnityEngine;
using UnityEngine.UI;
using TMPro;
using System.Text.RegularExpressions;

public class DialogueView : BaseView
{
    public Image imgAvatar;
    public TMP_Text textName;
    public TMP_Text textValue;
    public TypeWriter typeWriter;
    private void Start()
    {
        imgAvatar.sprite = null;
        textName.text = string.Empty;
        textValue.text = string.Empty;
    }

    /// <summary>
    /// 更新数据方法
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="model"></param>
    public override void UpdateView<T>(T model)
    {
        if (!(model is DialogueModel)) return;
        var dialogueModel = model as DialogueModel;
        imgAvatar.sprite = dialogueModel.Avatar;
        textName.text = dialogueModel.Name + ":";
        E_TypeWriterType type = E_TypeWriterType.Cover;
        float time = 0.05f;
        //测试
        foreach (var item in dialogueModel.Tags)
        {
            if (item == "Append")
            {
                type = E_TypeWriterType.Append;
            }
            else if (item.Split(':')[0] == "Time") 
            {
                time = float.Parse(item.Split(':')[1]);    
            }
        }
        typeWriter.WriteText(dialogueModel.Value, type , time);
    }
}

对话框SO

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;

[CreateAssetMenu(fileName = "DialogueSO", menuName = "SO/DialogueSO", order = 0)]
public class DialogueSO : ScriptableObject
{
    public DialogueData[] datas;
}
[Serializable]
public class DialogueData
{
    [Header("角色名字")]
    public string name;
    [Header("角色头像")]
    public string avatar;
    [TextArea]
    [Header("对话内容")]
    public string value;
    [Header("标签")]
    public string tag;
}

TextMeshPro打字机效果实现

using System.Collections;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using TMPro;
using UnityEngine;

/// <summary>
/// 打字机输入类型
/// </summary>
public enum E_TypeWriterType
{
    Append, Cover
}

/// <summary>
/// 打字机效果组件
/// </summary>
public class TypeWriter : MonoBehaviour
{

    [Header("文本框对象")]
    public TMP_Text textValue;

    [Header("默认文字间隔时间")]
    public float defaultTime = 0.2f;

    public bool IsTextShowDone => textValue.maxVisibleCharacters == StripHtmlTags(textValue.text).Length;//是否显示完成

    private Coroutine typingCoroutine;//当前携程

    private void Awake()
    {
        if (textValue == null) if (TryGetComponent(out textValue)) WriteText();
    }

    /// <summary>
    /// 输入文本
    /// </summary>
    /// <param name="text"></param>
    /// <param name="speed">如果不指定时间使用默认时间</param>
    public void WriteText(string text = null, E_TypeWriterType type = E_TypeWriterType.Cover, float? time = null)
    {
        if (textValue == null) return;
        if (typingCoroutine != null) StopCoroutine(typingCoroutine);
        switch (type)
        {
            case E_TypeWriterType.Cover:
                textValue.maxVisibleCharacters = 0;
                textValue.text = text ?? textValue.text;
                break;
            case E_TypeWriterType.Append:
                textValue.maxVisibleCharacters = StripHtmlTags(textValue.text).Length;
                textValue.text += text ?? string.Empty;
                break;
        }
        if (gameObject.activeSelf) typingCoroutine = StartCoroutine(TypeText(time == null ? defaultTime : (float)time));
    }

    /// <summary>
    /// 跳过当前输入
    /// </summary>
    public void SkipWriteText()
    {
        if (textValue == null) return;
        if (typingCoroutine != null) StopCoroutine(typingCoroutine);
        if (gameObject.activeSelf) typingCoroutine = StartCoroutine(TypeText(0f));
    }

    /// <summary>
    /// 去除富文本
    /// </summary>
    /// <param name="text"></param>
    /// <returns></returns>
    private string StripHtmlTags(string text) => Regex.Replace(text, "<.*?>", string.Empty);

    /// <summary>
    /// 打字机携程
    /// </summary>
    private IEnumerator TypeText(float time)
    {
        while (textValue.maxVisibleCharacters < StripHtmlTags(textValue.text).Length)
        {
            textValue.maxVisibleCharacters++;
            yield return new WaitForSeconds(time);
        }
    }
}

ScriptableObject

ScriptableObject是什么

ScriptableObject是Unity提供的一个数据配置存储基类

他是一个可以用来保存大量数据的数据容器

就像是可以自定义的数据资源文件

他是一个类似MonoBehvior的基类

需要我们继承他来使用

ScriptableObject的作用

  1. 数据复用(多个对象可以使用一个配置)
  2. 配置文件(配置数据中的数据)
  3. 编辑模式下的数据持久化

ScriptableObject的好处

  1. 我们可以直接在Inspector窗口编辑配置数据,可以利用它来做配置文件
  2. 处理重复数据,减少数据拷贝时造成的内存占用,可以利用它来做公共数据
  3. 可以方便的处理数据带来的多态行为

创建ScriptableObject数据容器

  1. 继承ScriptableObject类
  2. 在该类中声明成员(变量,方法等)
public clss Test : ScriptableObject
{
    public int a;
}

根据自定义的ScriptableObject数据容器创建数据文件

方法1:为类添加CreateAssetMenu通过菜单创建资源特性

[CreateAssetMenu(fileName = "默认文件名", menuName = "在Asset/Create菜单中显示的名字", order = 再Asset/Create菜单中的位置(多个时可以通过它来调整顺序))]
public clss Test : ScriptableObject
{
    public int a;
}

方法2:利用ScriptableObject的静态方法创建数据对象

public class ScriptableObjectTool
{
    [MenuItem("ScriptableObject/CreateMyData")]
    public static void CreateMyData()
    {
        //书写创建数据资源文件的代码
        MyData asset = ScriptableObject.CreateInstance<MyData>();
        //通过编辑器API 根据数据创建一个数据资源文件
        AssetDatabase.CreateAsset(asset, "Assets/Resources/MyDataTest.asset");
        //保存创建的资源
        AssetDatabase.SaveAssets();
        //刷新界面
        AssetDatabase.Refresh();
    }
}

ScriptableObject数据文件的使用

  1. Inspector面板中拖拽
  2. 通过资源加载的方式获取
 MyData data = Resources.Load<MyData>("MyDataTest");

ScriptableObject的生命周期函数

  • Awake 数据文件创建时调用
  • OnDestroy ScriptableObject 对象将被销毁时调用
  • OnDisable ScriptableObject 对象销毁时、即将重新加载脚本程序集时 调用
  • OnEnable ScriptableObject 创建或者加载对象时调用
  • OnValidate 编辑器才会调用的函数,Unity在加载脚本或者Inspector窗口中更改值时调用

在游戏运行中创建ScriptableObject对象(非持久化)

MyData data;
data = ScriptableObject.CreateInstance("MyData") as MyData;
//泛型
data = ScriptableObject.CreateInstance<MyData>();

Canvas Group

Canvas Group为面板父对象添加 CanvasGroup组件 即可整体控制一个面板的淡入淡出 或者 整体禁用

参数相关

  • Alpha:整体透明度控制
  • Interactable:整体启用禁用设置
  • Blocks Raycasts:整体射线检测设置
  • Ignore Parent Groups:是否忽略父级CanvasGroup的作用

UGUI异形按钮正确点击

方法一 通过添加子对象的形式

按钮之所以能够响应点击,主要是根据图片矩形范围进行判断的

它的范围判断是自下而上的,意思是如果有子对象图片,子对象图片的范围也会算为可点击范围

那么我们就可以用多个透明图拼凑不规则图形作为按钮子对象用于进行射线检测

方法二 通过代码改变图片的透明度响应阈值

  1. 修改图片参数 开启Read/Write Enabled开关
  2. 通过代码修改图片的响应阈值
//指定一个像素必须具有的最小alpha值,以变能够认为射线命中了图片
//当像素点alpha值小于了 该值 就不会被射线检测了
img.alphaHitTestMinimumThreshold = 0.1f;

数据持久化-JsonUtlity

JsonUtlity是什么?

  • JsonUtlity 是Unity自带的用于解析Json的公共类
  • 它可以将内存中对象序列化为Json格式的字符串
  • 将Json字符串反序列化为类对象

在文件中读取字符串

把字符串存入文件中

File.WriteAllText(Application.persistentDataPath + "/Test.json", "阿哈哈哈哈");

在指定路径的文件中读取字符串

string str = File.ReadAllText(Application.persistentDataPath + "/Test.json");//阿哈哈哈哈

使用JsonUtlity进行序列化

//JsonUtlity序列化方法 可以把对象 转为json字符串
string jsonStr = JsonUtility.ToJson(t);
//将字符串 保存到文件中
File.WriteAllText(Application.persistentDataPath + "/test.json", jsonStr);

注意:

  1. float序列化时看起来会有一些误差
  2. 成员中的自定义类需要加上序列化特性[System.Serializable]
  3. 想要序列化私有变量 需要加上特性[SerializeField]
  4. JsonUtility不支持字典
  5. JsonUtlity存储null对象不会是null 而是默认值的数据

使用JsonUtlity进行反序列化

//读取路径中的json
jsonStr = File.ReadAllText(Application.persistentDataPath + "/test.json");
//使用Json字符串内容 转换成类对象
Test t2 = JsonUtility.FromJson(jsonStr, typeof(Test)) as Test;
//使用泛型的方式反序列化
Test t3 = JsonUtility.FromJson<Test>(jsonStr);

注意:

数据持久化-LitJson

LitJson是什么

它是一个第三方库,用于处理Json的序列化和反序列化

/LitJson是C#编写的,体积小、速度快、易于使用

它可以很容易的嵌入到我们的代码中

只需要将LitJson代码拷贝到工程中即可

获取LitJson

前往LitJson官网

通过官网前往GitHub获取最新版本代码

将代码拷贝到Unity工程中 即可开始使用LitJson

使用LitJson进行序列化

//Litjson序列化方法
string jsonStr = JsonMapper.ToJson(t);
//将字符串保存到文件中
File.WriteAllText(Application.persistentDataPath + "/test.json", jsonStr);

注意:

  1. 相对JsonUtlity不需要在自定义类中加特性
  2. 不能序列化私有变量
  3. 支持字典类型,字典的键 建议都是字符串
  4. 需要引用LitJson命名空间
  5. LitJson可以准确的保存null类型

使用LitJson反序列化

//读取json文件
string jsonStr = File.ReadAllText(Application.persistentDataPath + "/test.json");
//使用泛型 反序列化
Test test = JsonMapper.ToObject<Test>(jsonStr);

注意:

  1. 类结构需要无参构造函数,否则反序列化时报错
  2. 字典虽然支持 但是键在使用为数值时会有问题 需要使用字符串类型
  3. Json文档编码格式必须是UTF-8
  4. LitJson可以直接读取数据集合

UGUI自动布局

自动布局是什么

虽然UGUI的RectTransform已经非常方便的可以帮助我们快速布局 但UGUI中还提供了很多可以帮助我们对UI控件进行自动布局的组件 他们可以帮助我们自动的设置UI控件的位置和大小等

自动布局的工作方式一般是

自动布局控制组件 + 布局元素 = 自动布局

自动布局控制组件:Unity提供了很多用于自动布局的管理性质的组件用于布局 布局元素:具备布局属性的对象们,这里主要是指具备RectTransform的UI组件

布局元素的布局属性

要参与自动布局的布局元素必须包含布局属性

布局属性主要有以下几条

  • Minmum width:该布局元素应具有的最小宽度
  • Minmum height:该布局元素应具有的最小高度
  • Preferred width:在分配额外可用宽度之前,此布局元素应具有的宽度
  • Preferred height:在分配额外可用高度之前,此布局元素应具有的高度。
  • Flexible width:此布局元素应相对于其同级而填充的额外可用宽度的相对量
  • Flexible height:此布局元素应相对于其同级而填充的额外可用宽度的相对量

在进行自动布局时都会通过计算布局元素中的这6个属性得到控件的大小位置 在布局时,布局元素大小设置的基本规则是

  1. 首先分配最小大小Minmum width和Minmum height
  2. 如果父类容器中有足够的可用空间,则分配Preferred width和Preferred height
  3. 如果上面两条分配完成后还有额外空间,则分配Flexible width和Flexible height

一般情况下布局元素的这些属性都是0 但是特定的UI组件依附的对象布局属性会被改变,比如Image和Text 一般情况下我们不会去手动修改他们,但是如果你有这些需求 可以手动添加一个Layout Element组件可以修改这些布局属性

水平垂直布局组件

水平垂直布局组件 将子对象并排或者竖直的放在一起 组件名:Horizontal Layout GroupVertical Layout Group 参数相关:

  • Padding:左右上下边缘偏移位置
  • Spacing:子对象之间的间距
  • ChildAlignment:九宫格对其方式
  • Control Child Size:是否控制子对象的宽高
  • Use Child Scale:在设置子对象大小和布局时,是否考虑子对象的缩放
  • Child Force Expand:是否强制子对象拓展以填充额外可用空间

网格布局组件

将子对象当成一个个的格子设置他们的大小和位置

组件名:Grid Layout Group

RectTransformUtility类

什么是RectTransformUtility类

RectTransformUtility 公共类是一个RectTransform的辅助类

主要用于进行一些 坐标的转换等等操作

其中对于我们目前来说 最重要的函数是 将屏幕空间上的点,转换成UI本地坐标下的点

将屏幕坐标转换为UI本地坐标系下的点

方法:RectTransformUtility.ScreenPointToLocalPointInRectangle

  • 参数1:相对父对象
  • 参数2:屏幕点
  • 参数3:摄像机
  • 参数4:最终得到的点

将屏幕上的一个点 转换为UI相对父物体的本地坐标一般配合拖拽事件使用

public void OnDrag(PointerEventData eventData)
{
    ////一般配合拖拽事件使用
    Vector2 nowPos;
    RectTransformUtility.ScreenPointToLocalPointInRectangle(
    parent,//父对象
    eventData.position,//PointerEventData提供的鼠标在屏幕上的位置
    eventData.enterEventCamera,//可以通过PointerEventData得到触发事件的摄像机
    out nowPos );
    this.transform.localPosition = nowPos;
}

UGUI模型和粒子特效显示在UI前面

模型显示在UI之前

方法一:

Canvas的渲染模式要不是覆盖模式

摄像机模式 和 世界(3D)模式都可以让模型显示在UI之前(Z轴在UI元素之前即可)

注意:

  1. 摄像机模式时建议用专门的摄像机渲染UI相关
  2. 面板上的3D物体建议也用UI摄像机进行渲染

方法二:

将3D物体渲染在图片上,通过图片显示

专门使用一个摄像机渲染3D模型,将其渲染内容输出到Render Texture上

该方式 不管Canvas的渲染模式是哪种都可以使用

粒子特效显示在UI之前

粒子特效的显示和3D物体类似

注意点:

在摄像机模式下时

可以在粒子组件的Renderer相关参数中改变排序层 让粒子特效始终显示在其之前不受Z轴影响

UGUI遮罩(Mask)

遮罩是什么

在不改变图片的情况下

让图片在游戏中只显示其中的一部分

遮罩如何使用

实现遮罩效果的关键组件时Mask组件

通过在父对象上添加Mask组件即可遮罩其子对象

注意:

  1. 想要被遮罩的Image需要勾选Maskable
  2. 只要父对象添加了Mask组件,那么所有的UI子对象都会被遮罩
  3. 遮罩父对象图片的制作,不透明的地方显示,透明的地方被遮罩