Unity-HotFix

学习自火星网校。

资源

正文

编辑器开发

01 编辑器开发基础介绍

如此构建项目:

  • Assets
    • Scenes
    • Scripts
      • MultiResources.cs
    • TestA
      • Resources
        • Cube.prefab
    • TestB
      • Resources
        • Sphere.prefab

Main Camera 绑一个 MultiResources.cs

C#
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
 
public class MultiResources : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        Object cube = Resources.Load("Cube");
        GameObject obj1 = Instantiate(cube) as GameObject;
 
        Object sphere = Resources.Load("Sphere");
        GameObject obj2 = Instantiate(sphere) as GameObject;
    }
}
webp

运行程序,预制体被创建,这说明 Resources/ 是 Unity 中的一个特殊文件夹(存储跟随游戏包的资源目录)

注意

  • Plugins:需要跨语言调用的代码逻辑代码存储目录,手机 SDK 接入
  • Resources:存储跟随游戏包的资源目录
  • StreamingAssets:只读,存储跟随游戏包的资源目录

02-03 编辑器检视器面板简单扩展 1

注意

用于在 C# 运行时,传递程序中各种元素(类,结构体,变量,方法,枚举,组件)的行为信息的声明标签。一个声明标签是通过放置在它所在应用元素的前面的方括号“[]”中来描述。

写一个 Assets/Scripts/SimpleInspector.cs

C#
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
 
// 对象如果不标记为可序列化,则 Unity 在存储的时候,会认为他不可被序列化,那么也就无法被显示
// Unity 的内置 JSON 工具运行原理与之类似
[Serializable]
public class Numerical
{
    public int Atk;
    public int Def;
}
 
// 职业枚举
public enum Profession
{
    Warrior = 0,
    Wizard,
}
 
public class SimpleInspector : MonoBehaviour
{
    // 隐藏公共成员变量,防止 Inspector 的值影响到它
    // 同时保证脚本中变量的可访问度
    [HideInInspector]
    public int ID = 99;
 
    // 私有变量,检视面板可见
    // Unity 会将对象进行序列化存储,所以即使是私有的,那么标记为可序列化后,就会显示,共有默认值是可序列化的
    [SerializeField]
    private string Name;
 
    // 监视面板显示对象
    public Numerical Num;
 
    // 把当前成员变量上方留 50 像素空白区域
    [Space(50)]
 
    // 当前成员变量上方加入一个标题文字
    [Header("年龄")]
 
    // 添加变量悬浮提示文字
    // 一个成员变量可以添加多个特性
    [Tooltip("不要填写大于 150 岁的年龄")]
    // 给数值设定范围(最小 0,最大 150)
    [Range(0, 150)]
    public int Age;
 
    // 指定输入框,拥有五行
    [Multiline(5)]
    public string NickName;
 
    // 默认显示五行,最多显示十行内容,再多用滚动条控制显示区域
    [TextArea(5, 10)]
    public string Description;
 
    public Color Flag;
 
    public Texture Tex;
 
    public List<string> Tags;
 
    public Profession Pro;
 
    // 给小齿轮添加一个回调函数
    [ContextMenu("输出攻防比:")]
    public void PrintADProportion()
    {
        Debug.Log("攻击/防御比例:" + Num.Atk + "/" + Num.Def);
    }
 
    // 给一个成员变量添加右键菜单
    // 第一个参数是菜单的名称
    // 第二个参数是右键点击的回调函数
    [ContextMenuItem("输出国家", "OutCountry")]
    [Tooltip("右键菜单")]
    public string Country;
 
    public void OutCountry()
    {
        Debug.Log(Country);
    }
}
webp webp webp
C#
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
 
// 将 Player 组件添加到 AddComponent 上
// 第一个参数:分类名/组件名
// 第二个参数:列表中显示的顺序
[AddComponentMenu("自定义控制器/玩家控制器", 1)]
// 使生命周期函数,在编辑器状态下可以执行,游戏中也可以正常使用
// Update() 在场景中对象发生变化或项目组织发生变化时会在编辑器下执行
[ExecuteInEditMode]
// 关于类型和类名
// BoxCollider:是类名,适用于函数提供泛型方法
// typeof(BoxCollider):System.Type,C# 的类型,适用于函数需要 System.Type 参数
 
// 当前组件依赖于盒子碰撞体
// 当前组件挂载在对象时,盒子碰撞体会一起被添加上去
// 当 Player 组件没有被移除时,盒子碰撞体不能被删除
[RequireComponent(typeof(BoxCollider))]
public class Player : MonoBehaviour
{
    // Update is called once per frame
    void Update()
    {
        Debug.Log("Update");
    }
}
 
webp

创建一个多选的列表:

C#
[Flags]
public enum PlayerLoveColor
{
    Green = 1,
    Red = 2,
    Pink = 4,
}
webp

04-05 编辑器检视器面板深度扩展 1

编辑 Assets/Scripts/Player.cs

C#
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
 
// 职业枚举
public enum PlayerProProfession
{
    Warrior = 0,
    Wizard,
}
 
[Flags]
public enum PlayerLoveColor
{
    Green = 1,
    Red = 2,
    Pink = 4,
}
 
// 将 Player 组件添加到 AddComponent 上
// 第一个参数:分类名/组件名
// 第二个参数:列表中显示的顺序
[AddComponentMenu("自定义控制器/玩家控制器", 1)]
// 使生命周期函数,在编辑器状态下可以执行,游戏中也可以正常使用
// Update() 在场景中对象发生变化或项目组织发生变化时会在编辑器下执行
[ExecuteInEditMode]
// 关于类型和类名
// BoxCollider:是类名,适用于函数提供泛型方法
// typeof(BoxCollider):System.Type,C# 的类型,适用于函数需要 System.Type 参数
 
// 当前组件依赖于盒子碰撞体
// 当前组件挂载在对象时,盒子碰撞体会一起被添加上去
// 当 Player 组件没有被移除时,盒子碰撞体不能被删除
[RequireComponent(typeof(BoxCollider))]
public class Player : MonoBehaviour
{
    public int ID;
    public string Name;
    public float Atk;
    public bool isMan;
    public Vector3 HeadDir;
    public Color Hair;
    public GameObject Weapon;
    public Texture Cloth;
    public PlayerProProfession Pro;
    public PlayerLoveColor LoveColor;
 
    // Update is called once per frame
    void Update()
    {
        Debug.Log("Update");
    }
}

创建 Assets/Editor/PlayerEditor.cs

C#
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
 
// 步骤 1:引入编辑器的命名空间,检视器属于编辑器开发范畴
using UnityEditor;
 
[CustomEditor(typeof(Player))]  // 步骤 3:将编辑器开发脚本与需要编辑的组件脚本建立外挂关联关系
// 外挂脚本因为存储在 Editor 目录下,所以不会被打入最终的游戏包
// 不继承 Mono,而是继承自 Editor
public class PlayerEditor : Editor  // 步骤 2:继承 Editor 类,使用编辑器相关的成员变量和生命周期函数
{
    // 获得到需要编辑显示的组件
    private Player _Componet;
 
    // 当关联组件所在对象被选中或组件被添加时,调用
    private void OnEnable()
    {
        _Componet = target as Player;
    }
 
    // 当关联组件所在对象被取消或组件被移除时,调用
    private void OnDisable()
    {
        _Componet = null;
    }
 
    // 用于绘制检视面板的生命周期函数
    public override void OnInspectorGUI()
    {
        // 标题显示
        EditorGUILayout.LabelField("人物相关属性");
 
        // 整型
        _Componet.ID = EditorGUILayout.IntField("玩家 ID", _Componet.ID);
        // 文本
        _Componet.Name = EditorGUILayout.TextField("玩家名称", _Componet.Name);
        // 浮点数
        _Componet.Atk = EditorGUILayout.FloatField("玩家攻击力", _Componet.Atk);
        // 布尔值
        _Componet.isMan = EditorGUILayout.Toggle("是否为男性", _Componet.isMan);
        // 向量
        _Componet.HeadDir = EditorGUILayout.Vector3Field("头部方向", _Componet.HeadDir);
        // 颜色
        _Componet.Hair = EditorGUILayout.ColorField("颜色", _Componet.Hair);
 
        // 对象数据类型绘制
        // 参数 1:标题
        // 参数 2:原始组件的值
        // 参数 3:成员变量的类型
        // 参数 4:是否可以将场景中的对象拖给这个成员变量
        _Componet.Weapon = EditorGUILayout.ObjectField("武器", _Componet.Weapon, typeof(GameObject), true) as GameObject;
        // 纹理
        _Componet.Cloth = EditorGUILayout.ObjectField("衣服材质贴图", _Componet.Cloth, typeof(Texture), true) as Texture;
 
        // 枚举数据类型绘制
        // 单选枚举
        _Componet.Pro = (PlayerProProfession)EditorGUILayout.EnumPopup("职业", _Componet.Pro);
        // 多选枚举
        _Componet.LoveColor = (PlayerLoveColor)EditorGUILayout.EnumFlagsField("爱好", _Componet.LoveColor);
        // 终极数据类型绘制
        // 更新可序列化数据
        serializedObject.Update();
        // 通过成员变量名找到组件上的成员变量
        SerializedProperty sp = serializedObject.FindProperty("Items");
        // 可序列化数据绘制(取到的数据,标题,是否将所有获得的序列化数据显示出来)
        _Componet.Atk = EditorGUILayout.Slider(new GUIContent("攻击力"), _Componet.Atk, 0, 100);
 
        if (_Componet.Atk > 80)
        {
            // 显示消息框(红色)
            EditorGUILayout.HelpBox("攻击力过高", MessageType.Error);
        }
 
        if (_Componet.Atk < 20)
        {
            // 显示消息框(黄色)
            EditorGUILayout.HelpBox("攻击力过低", MessageType.Warning);
        }
 
        // 按钮显示和元素排列
        // (按钮是否被按下)显示按钮(按钮名称)
        GUILayout.Button("来个按钮");
        GUILayout.Button("来个按钮");
 
        // 开始横向排列绘制
        EditorGUILayout.BeginHorizontal();
 
        GUILayout.Button("再来个按钮");
        GUILayout.Button("再来个按钮");
 
        // 结束横向排列绘制
        EditorGUILayout.EndHorizontal();
    }
}

此时就会修改 Player 的显示面板:

webp

06 编辑器菜单栏扩展

创建 Assets/Editor/Menu.cs

C#
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
 
// 顶部菜单类
public class Menu {
    // 在顶部显示“工具”菜单,下方有“导出 AB 资源包”,点击执行函数
    [MenuItem("工具/导出 AB 资源包")]
    static void BuildAB()
    {
        Debug.Log(Application.persistentDataPath);
    }
}
webp

07 编辑器场景视窗扩展

创建 Assets/Editor/PopWindow.cs

C#
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
 
public class PopWindow : EditorWindow
{
    [MenuItem("工具/创建窗口")]
    static void OpenWindow()
    {
        PopWindow window = GetWindow<PopWindow>(false, "弹窗标题", true);
        window.minSize = new Vector2(400, 300);
        window.maxSize = new Vector2(800, 600);
    }
 
    // 开窗口调用
    private void OnEnable()
    {
        Debug.Log("OnEnable");
    }
 
    // 关闭窗口调用
    private void OnDisable()
    {
        Debug.Log("OnDisable");
    }
 
    // 更新
    private void Update()
    {
        Debug.Log("Update");
    }
 
    private void OnGUI()
    {
        if (GUILayout.Button("测试点击"))
        {
            Debug.Log("测试点击");
        }
    }
 
    // 场景结构发生变化,执行回调函数
    private void OnHierarchyChange()
    {
        Debug.Log("hierarchy");
    }
 
    // 项目结构发生变化,执行回调函数
    private void OnProjectChange()
    {
        Debug.Log("project");
    }
 
    // 选中物体发生变化,执行回调函数
    private void OnSelectionChange()
    {
        Debug.Log(Selection.activeGameObject.name);
    }
}
webp

创建一个 Assets/Scripts/NodeManager.cs

C#
using System.Collections.Generic;
using UnityEngine;
 
[ExecuteInEditMode]
public class NodeManager : MonoBehaviour
{
    public List<GameObject> nodes = new List<GameObject>();
 
    void OnDrawGizmos()
    {
        if (nodes == null || nodes.Count < 2)
            return;
 
        Gizmos.color = Color.red;
        for (int i = 0; i < nodes.Count - 1; i++)
        {
            Gizmos.DrawLine(nodes[i].transform.position, nodes[i + 1].transform.position);
        }
    }
}

创建一个 Assets/Editor/NodeManagerEditor.cs

C#
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
 
public class NodeWindow : EditorWindow
{
    static NodeWindow window;
 
    static GameObject nodeManager;
 
    public static void OpenWindow(GameObject manager)
    {
        nodeManager = manager;
        // 真正开启了一个窗口
        window = EditorWindow.GetWindow<NodeWindow>();
    }
 
    void Update()
    {
        // 通过窗口的 Update,每帧执行一次,当前被选中的对象为板子
        Selection.activeGameObject = nodeManager;
    }
 
    public static void CloseWindow()
    {
        window.Close();
    }
}
 
// 外挂式关联 NodeManager
[CustomEditor(typeof(NodeManager))]
public class NodeManagerEditor : Editor
{
 
    NodeManager manager;
 
    bool isEditor = false;  // 是否是编辑的状态
 
    // 当选中带有 NodeManager 组件对象的时候,获得组件
    void OnEnable()
    {
        manager = (NodeManager)target;
    }
 
    // 绘制组件的生命周期函数
    public override void OnInspectorGUI()
    {
        // 通过终极的数据获取方法,显示列表中的数据
        serializedObject.Update();
        SerializedProperty nodes = serializedObject.FindProperty("nodes");
        EditorGUILayout.PropertyField(nodes, new GUIContent("路径"), true);
        serializedObject.ApplyModifiedProperties();
 
        // 开始编辑的开关
        if (!isEditor && GUILayout.Button("开始编辑"))
        {
            NodeWindow.OpenWindow(manager.gameObject);  // 调用打开界面的方法
            isEditor = true;  // 改变状态变成编辑模式
        }
        // 结束编辑的开关
        else if (isEditor && GUILayout.Button("结束编辑"))
        {
            NodeWindow.CloseWindow();  // 调用关闭界面的方法
            isEditor = false;  // 改变状态变成非编辑模式
        }
 
        // 删除按钮
        if (GUILayout.Button("删除最后一个节点"))
        {
            RemoveAtLast();
        }
        // 删除所有按钮
        else if (GUILayout.Button("删除所有节点"))
        {
            RemoveAll();
        }
    }
 
    RaycastHit hit;
 
    // 有点类似前期 Update 函数,发送射线
    // 当选中关联的脚本挂载的物体
    // 当鼠标在 Scene 视图下发生变化时,执行该方法,比如鼠标移动,比如鼠标的点击
    void OnSceneGUI()
    {
 
        if (!isEditor)  // 非编辑状态下不能生成路点
        {
            return;
        }
 
        // 当鼠标按下左键时发射一条射线 
        // 非运行时,使用 Event 类
        // Event.current.button 判断鼠标是哪个按键的(0 是鼠标左键)
        // Event.current.type 判断鼠标的事件方式的(鼠标按下)
        if (Event.current.button  0 && Event.current.type  EventType.MouseDown)
        {
            // 从鼠标的位置需要发射射线了
            // 因为是从 Scene 视图下发射射线,跟场景中的摄像机并没有关系,所以不能使用相机发射射线的方法
            // 从编辑器 GUI 中的一个点向世界定义一条射线, 参数一般都是鼠标的坐标
            Ray ray = HandleUtility.GUIPointToWorldRay(Event.current.mousePosition);
            if (Physics.Raycast(ray, out hit, 100))
                {
                    // 需要在检测到的点实例化,路点
                    InstancePathNode(hit.point + Vector3.up * 0.1f);
            }
 
        }
    }
 
    /// <summary>
    /// 生成节点
    /// </summary>
    /// <param name="position"></param>
    void InstancePathNode(Vector3 position)
    {
        // 点预制体
        GameObject prefab = Resources.Load<GameObject>("PathNode");
        // 点对象,生成到 Plane 的子物体下
        GameObject pathNode = Instantiate<GameObject>(prefab, position, Quaternion.identity, manager.transform);
        // 把生成的路点添加到列表里
        manager.nodes.Add(pathNode);
    }
 
    /// <summary>
    /// 删除最后一个节点
    /// </summary>
    void RemoveAtLast()
    {
        // 保证有节点才能删节点
        if (manager.nodes.Count > 0)
        {
            // 从场景中删除游戏物体
            DestroyImmediate(manager.nodes[manager.nodes.Count - 1]);
            // 把该节点从列表中移除
            manager.nodes.RemoveAt(manager.nodes.Count - 1);
        }
 
    }
 
    /// <summary>
    /// 删除所有的节点
    /// </summary>
    void RemoveAll()
    {
        // 遍历删除所有的节点物体
        for (int i = 0; i < manager.nodes.Count; i++)
        {
            if (manager.nodes[i] != null)
            {
                DestroyImmediate(manager.nodes[i]);
            }
        }
 
        manager.nodes.Clear();  // 清空列表
    }
}

创建一个 Assets/Resources/PathNode.prefab

webp webp

AB 包

08 复习及今天介绍

注意

AB 包和 Resources 的区别

  • 存储
    • Resources 内部资源存储在游戏的发布包中,
    • AB 包存储在独立的文件中(AB 包存储在非特殊目录下时,不在游戏的发布包中)。
  • 加载
    • Resources 内部资源使用 Resources.Load()
    • AB 包(可以理解为可下载的 Resources
      • 获得 AB 包文件(下载,解压 streaming 拷贝到可写目录)
      • 加载步骤 1:通过 AB 包文件路径,加载 AB 包文件
      • 加载步骤 2:通过名称(资源,o)加载内部资源

AssetBundle 的定义

AssetBundle 是把一些资源文件,场景文件或二进制文件以某种紧密的方式保存在一起的一些文件。

AssetBundle 内部不能包含 C# 脚本文件,AssetBundle 可以配合 Lua 实现资源和游戏逻辑代码的更新。

AssetBundle 是独立于游戏主包存在的资源存储文件,使用内部资源时,需要单独下载和加载。

webp

热更新

不关闭 Unity 应用的前提,实现游戏资源和代码逻辑的更新。

09 AB 包介绍及导出工具实现

webp

选中若干资源,打包成 AssetBundle(给其命名)。

创建 Assets/Editor/ExportAB.cs

C#
using UnityEngine;
using UnityEditor;
using System.IO;
 
public class ExportAB
{
    [MenuItem("AB 包/导出")]
    public static void Export()
    {
        Debug.Log("导出 AB 包");
        // 项目的 Assets 目录的路径
        string path = Application.dataPath;
        path = path.Substring(0, path.Length - 6) + "ab";
 
        // 防止路径不存在
        if (!Directory.Exists(path))
        {
            Directory.CreateDirectory(path);
        }
 
        // 导出 AB 包的核心代码,生成 AB 包文件
        // 参数 1:AB 文件存储路径
        // 参数 2:导出选项
        // 参数 3:平台(不同平台的 AB 包是不一样的)
        BuildPipeline.BuildAssetBundles(
            path,
            BuildAssetBundleOptions.None,
            BuildTarget.StandaloneWindows);
 
        Debug.Log("导出完成");
    }
}

注意

BuildAssetBundleOptions 是 Unity 提供的一组标志枚举,用于控制 BuildPipeline.BuildAssetBundles 方法的行为。以下是常见的选项:

常见的 BuildAssetBundleOptions 选项:

选项说明
None无特殊选项,使用默认的打包方式。
UncompressedAssetBundle生成未压缩的 AB 包,加载更快但占用空间更大。
DisableWriteTypeTree禁用类型树,减少文件大小,但可能会导致跨版本不兼容。
DeterministicAssetBundle生成一致的 AB 文件,确保相同资源的哈希值相同,以便版本管理。
ForceRebuildAssetBundle强制重新构建所有 AB 文件,而不使用已有的缓存。
IgnoreTypeTreeChanges忽略类型树的更改,提高跨 Unity 版本的兼容性。
AppendHashToAssetBundleName在 AB 文件名后追加哈希值,以确保版本更新时不会覆盖旧版本。
ChunkBasedCompression使用 LZ4 压缩,提高运行时解压速度,适用于频繁加载的资源。
StrictMode在构建时启用严格模式,检测可能的错误并抛出异常。
DryRunBuild进行模拟构建,但不会实际生成 AB 文件,可用于调试。

10 AB 包资源简单加载

创建 Assets/Scripts/Config.cs

C#
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
 
public class Config
{
    public static string ABPath = Application.dataPath.Substring(0, Application.dataPath.Length - 6) + "ab";
}

创建 Assets/Scripts/SimpleLoad.cs 并绑定在场景的某个组件中:

C#
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
 
public class SimpleLoad : MonoBehaviour
{
    public Image Icon;
    // Start is called before the first frame update
    void Start()
    {
        // 第一步加载 AB 文件
        AssetBundle ab = AssetBundle.LoadFromFile(Config.ABPath + "/ui");
 
        // 第二步加载资源
        Sprite sp = ab.LoadAsset<Sprite>("参考图");
 
        GameObject.Find("/Canvas/Image").GetComponent<Image>().sprite = sp;
 
        Debug.Log("加载完成" + sp.name);
 
        Icon.sprite = sp;
 
        ab.Unload(false);
    }
}
webp

11 资源热更新演示

导入资源中的 AB 包:ab/old/testab/New/test

创建 Assets/Scripts/HotUpdate.cs 并导入到场景中:

C#
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
 
public class HotUpdate : MonoBehaviour
{
    public Image Icon;
    // Start is called before the first frame update
    void Start()
    {
        AssetBundle ab = AssetBundle.LoadFromFile(Config.ABPath + "Old/test");
        Sprite sp = ab.LoadAsset<Sprite>("btn_go_back");
 
        Icon.sprite = sp;
        ab.Unload(false);
    }
 
    // 点击按钮后,加载新 AB 包里面的 btn_go_back 资源
    public void ChangeAB()
    {
        AssetBundle ab = AssetBundle.LoadFromFile(Config.ABPath + "New/test");
        Sprite sp = ab.LoadAsset<Sprite>("btn_go_back");
 
        Icon.sprite = sp;
        ab.Unload(false);
    }
}
webp

Button 绑定 ChangeAB(),后,即可按下按钮更改图片。

12 优化导出工具及依赖加载

编辑 ExportAB.cs,将 AB 包导出至不同的平台:

C#
[MenuItem("AB 包导出/Windows")]
public static void ForWindows()
{
    Export(BuildTarget.StandaloneWindows);
}
 
[MenuItem("AB 包导出/Mac")]
public static void ForMac()
{
    Export(BuildTarget.StandaloneOSX);
}
 
[MenuItem("AB 包导出/iOS")]
public static void ForiOS()
{
    Export(BuildTarget.iOS);
}
 
[MenuItem("AB 包导出/Android")]
public static void ForAndroid()
{
    Export(BuildTarget.Android);
}

12 优化导出工具及依赖加载

注意

加载 AB 包内部数据

如果想处理依赖关系的加载,则必须加载主AB包,因为依赖关系的存储,都存储在主 AB 包的配置文件中

第一步(加载依赖的 AB 包文件)

  1. 加载主 AB 包

  2. 根据主 AB 包的配置文件,获得我当前需要加载的 AB 所依赖的 AB 们

  3. 将所有的依赖 AB 们,加载进来

第二步(加载 AB 包文件)

  • AB 包 = AssetBundle.LoadFromFile(AB 包文件路径)
  • AssetBundle.LoadFromFileSync(AB 包文件路径)

第三步(加载 AB 包内部资源)

  • 资源对象 = AB 包对象.LoadAsset<资源类型>(“资源名称”)

  • AB 包对象.LoadAssetSync<资源类型>(“资源名称”)

注意!!!:AB 包不能重复加载

C#
public class Load : MonoBehaviour
{
    void Start()
    {
        // 加载主 AB 包
        AssetBundle main = AssetBundle.LoadFromFile(Config.ABPath + "/ab");
        
        // 获取主 AB 包的配置文件
        AssetBundleManifest manifest = main.LoadAsset<AssetBundleManifest>("AssetBundleManifest");
        
        // 分析预制体所在 AB 包,依赖哪些 AB 包
        // deps 存储了所有依赖的 ab 包的名字
        string[] deps = manifest.GetAllDependencies("test2");
        
        // 加载依赖的 AB 包
        for(int i = 0; i < deps.Length; i++)
        {
            AssetBundle.LoadFromFile(Config.ABPath + "/" + deps[i]);
        }
        
        // 加载预制体所在的 AB 包
		AssetBundle test2 = AssetBundle.LoadFromFile(Config.ABPath + "/test2");
        
        // 加载预制体
        Object prefab = test2.LoadAsset("Image");
        
		Gameobject img = Instantiate(prefab) as Gameobject;
        img.transform.SetParent(GameObject.Find("/Canvas").transform);
    }
}

13 异步加载

使用协程 IEnumerator 进行异步加载(Resources / AB 包 两种方式),创建 Assets/Scripts/AsyncLoad.cs

C#
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
 
public class AsyncLoad : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        // StartCoroutine(LoadImage());
        StartCoroutine(LoadAB());
    }
 
    IEnumerator LoadImage()
    {
        ResourceRequest rr = Resources.LoadAsync<Sprite>("参考图");
        yield return rr;
 
        GameObject.Find("/Canvas/Image").GetComponents<Image>()[0].sprite = rr.asset as Sprite;
    }
 
    IEnumerator LoadAB()
    {
        AssetBundleCreateRequest abcr = AssetBundle.LoadFromFileAsync(Config.ABPath + "/ui");
        yield return abcr;
 
        GameObject.Find("/Canvas/Image").GetComponents<Image>()[0].sprite = abcr.assetBundle.LoadAsset<Sprite>("参考图");
    }
}

14 内存分析

创建一个 Assets/Scripts/Memory.cs

C#
using UnityEngine;
using UnityEngine.UI;
 
public class Memory : MonoBehaviour
{
    AssetBundle ab;
    public Image img;
 
    public void LoadFile()
    {
        Debug.Log("LoadFile()");
        if (ab == null)
        {
            ab = AssetBundle.LoadFromFile(Config.ABPath + "/ui");
        }
    }
 
    public void LoadImage()
    {
        Debug.Log("LoadImage()");
        if (ab!= null)
        {
            img.sprite = ab.LoadAsset<Sprite>("参考图");
        }
 
    }
 
    public void UnloadFile()
    {
        Debug.Log("UnloadFile()");
        if (ab != null)
        {
            ab.Unload(true);  // 参数代表,是否将场景中通过 AB 包加载出来的资源,同 AB 包一起销毁
        }
    }
 
    public void Recycling()
    {
        Debug.Log("Recycling()");
        Resources.UnloadUnusedAssets();  // 将所有没有使用过的资源进行回收
    }
}
webp

Ctrl + 7 即可调用出内存分析界面,在加载 AB 包后,内存使用量会上升。

webp

Lua

15 Lua 环境搭建

安装 LuaForWindows_v5.1.5-52.exe

webp

这个仓库可以让你在线使用 Jupyter Notebook 进行 Lua 的编写。

16 Lua 变量与数据类型

Lua 注释:

lua
-- Lua 的单行注释
 
--[[
    这是 Lua 的多行注释
]]

变量操作:

lua
name = "hxsd"
print(name)
hxsd
lua
print(_VERSION)
Lua 5.4
lua
print(id)
nil
lua
name = nil
print(name)
nil
lua
local name = "Unity"
print(type(name))
print(type(type(name)))
string 
string
lua
print(type(bb))
nil
lua
print(type(bb) == "nil")
true
lua
print(type(123))
print(type(1.0))
number 
number
lua
print(type(true))
boolean
lua
print(type('hxsd'))
string

17 Lua 字符串操作

lua
local str1 = "Abc"
local str2 = 'def'
 
print(str1 .. str2)
 
print(#str1)
 
print(string.upper(str1))
 
print(string.lower(str1))
Abcdef 
3 
ABC 
abc 
lua
-- 多行字符串
local str3 = [[
one
two
]]
lua
print("1" + "2")
3
lua
-- 字符串查找
-- 参数 1:被查找的字符串
-- 参数 2:查找的内容
-- 返回值起始找到的位置和结束找到的位置
print(string.find("abcdefg", "cde"))
3 5
lua
-- 字符串反转
print(string.reverse("abcdefg"))
gfedcba
lua
print(string.sub("abcdefg", 3))
cdefg
lua
print(string.sub("abcdefg", 3, 6))
cdef
lua
local data = "abcdefghijkm"
print(string.sub(data, 3, #data - 1))
cdefghijk
lua
print(string.format("I'm the %d player, other is %d", 1, 2))
I'm the 1 player, other is 2
lua
print(string.rep("abc", 2))
abcabc
lua
print(string.char(65))
A
lua
print(string.byte("A"))
65
lua
-- 字符串替换
-- 参数 1:原始串
-- 参数 2:需要替换的内容
-- 参数 3:替换的内容
-- 返回替换后的字符串及匹配次数
print(string.gsub("abcd", "bc", "**"))
a**d 1
lua
print((string.gsub("abcd", "bc", "**")))
a**d
lua
local new_str = string.gsub("abcd", "bc", "**")
print(new_str)
a**d

18 Lua 逻辑控制

lua
local data = {}
-- 起始索引是 1 开始
-- 类型可以混合
-- 索引值可以为负数
-- 即使索引从 1 开始,也可以赋值 0 索引
-- 索引可以断开
-- 初始化时对于没有索引的值,索引是从 1 向上累加的
-- 初始化提供索引的赋值方法,[索引值] = 数值
data = {"abc", 123, [-1] = 100, [0] = 99, [4] = 233}
 
print(data[1])
print(data[2])
print(data[-1])
print(data[0])
print(data[4])
print(data[123])
 
-- 获取数组的长度
-- 这种方式,获得的是从 1 案引开始,索引连续的数据个数,中间断开,计数结束
-- 这种方式不稳定
print(#data)
 
-- 修改某一个值
data[1] = "def"
print(data[1])
 
local data2 = {{"aa", "bb"}, {11, 22}}
print(data2[2][1])
abc 
123 
100 
99 
233 
nil 
2 
def 
11 
lua
print(2 ^ 3)
8.0
lua
print(2 ^ 0.5)
1.4142135623731
lua
print(2 ~= 3)
true
  • Lua 没有 ++
  • Lua 没有 +=-=*=
lua
if(false)
then
    print("条件 1 达成")
elseif(true)
then
    print("elseif 达成")
else
    print("else 达成")
end
elseif 达成
lua
if(true)
then
    if(true)
    then
        print("进入第二层 if")
    end
end
进入第二层 if
lua
local num = 1
while (num < 3)
do
    print(num)
    num = num + 1
end
1 
2 

19 Lua 语法介绍

lua
local num = 1
 
repeat
    print(num)
    num = num + 1
until(num > 5)
1 
2 
3 
4 
5
lua
local data = {"aa", "bb", "cc", "dd", "ee"}
 
-- 参数 1:变量 i 的初始值,遍历 Lua 表,使用 1
-- 参数 2:增长到多少
-- [参数 3]:增长步长,默认值是 1
for i = 1, #data, 2
do
    print(data[i])
end
aa 
cc 
ee
lua
-- break 会跳出循环,没有 continue
repeat
    if(true)
    then
        print("此处跳出")
        break
    end
until(true)
 
print("继续执行")
此处跳出 
继续执行
lua
-- 迭代器(遍历 table)
-- one 是不加中括号的字符串索引
-- "aa" "bb"自动加 1,2 索引
-- [4] 指定数字索引
-- [-1] 指定负数索引
-- ["two"] 是加中括号的字符串密引写法
local data = {one = "cc", "aa", "bb", [4] = 3, [-1] = 4, ["two"] = "dd"}
 
-- 连续索引数值迭代器
-- 迭代器就是指向table的指针
-- 连续数字索引迭代器,只会获取从 1 开始的数字索引,且必须索引是连续的才能持续获得值
for k, v in ipairs(data)
do
    print("k: " .. k .. ", v: " .. v)
end
 
print("-------------------")
 
-- 所有数值迭代器
-- 相对于 ipairs,所有数值迭代器去掉了 i
-- i 可以理解为 int,去掉 int 的迭代器,也就去掉了连续数值案引的限定
-- 获取 table 长度的最稳定方法,就是使用所有数值迭代器获取
for k, v in pairs(data)
do
    print("K: " .. k .. ", V: " .. v)
end
k: 1, v: aa 
k: 2, v: bb 
------------------- 
K: 1, V: aa 
K: 2, V: bb 
K: -1, V: 4 
K: 4, V: 3 
K: one, V: cc 
K: two, V: dd

20 Lua 函数与表

lua
function func1()
    print("这是 func1")
end
 
func1()
这是 func1
lua
local func3 = function(a, b)
    print(a + b)
end
 
func3(5, 7)
func3(5, 7, 9)  -- 参数可以多出来
12 
12 
lua
local func4 = function(...)
    -- 将无固定参数,转换为 table
    -- arg 的作用域是 func4 的函数体
    local arg = {...}
    local total = 0
 
    for k, v in pairs(arg)
    do
        total = total + v
    end
 
    print(total)
end
 
func4(1, 2, 3)
func4(1, 2, 3, 4, 5)
6 
15
lua
function func5()
    return 99, 100
end
 
print(func5())
 
-- 将多返回值,同时赋值给两个变量
local num1, num2 = func5()
print(num1)
print(num2)
99 100 
99 
100 
lua
local data = {one = "cc", "aa", "bb", [4] = 3, [-1] = 4, ["two"] = "dd"}
 
print(data[2])
print(data["one"])
print(data.two)
 
-- 因为函数是一种数据类型
-- 所以将 func1 索引下定义了一个函数
data.func1 = function()
    print("data 表中的 func1 函数")
end
 
data.func1()
 
data.func2 = function()
    print(data.two)
end
 
data.func2()
 
-- 成员函数定义时,显式加入 self 变量,对应 C# 的 this 关键字
-- 函数内部可以通过 self 变量获取当前 table 的其他值或函数
data.func3 = function(self)
    print(self.two)
end
 
data:func3()
 
-- 第二种 self 调用写法
-- 隐式给 self 赋值
function data:func4()
    print("func3: " .. self.two)
end
 
data:func4()
bb 
cc 
dd 
data 表中的 func1 函数 
dd 
dd 
func3: dd 

21 Lua 子文件与元表

创建 appName.lua

lua
local config = {}
 
config.appName = "hxsd"
 
return config
lua
package.loaded["test"]
true
lua
local config = require("appName")
print(config.appName)
hxsd

元表:Lua 元表(Metatable) | 菜鸟教程

lua
local t1 = {1, 2, 3}
print(t1)
 
local meta = {
    __tostring = function(t)
        local format = "{"
        for k, v in pairs(t)
        do
            format = format .. v .. ', '
        end
    
        format = format .. "}"
        return format
    end
}
 
setmetatable(
    t1,
    meta
);
 
print(t1)
{ 1, 2, 3 } 
[[{1, 2, 3, }]]

这里说直接 print() 列表会输出内存地址,然而这里并没有,是不是 Lua 更新了?

xLua

23 热更新简介

注意

什么是冷更新

开发者将测试好的代码,发布到应用商店的审核平台,平台方会进行稳定性及性能测试。测试成功后,用户即可在 AppStore,看到应用的更新信息,用户点击应用更新后,需要先关闭应用,再进行更新。

什么是热更新

广义:无需关闭应用,不停机状态下修复漏洞,更新资源等,重点是更新逻辑代码。

狭义定义(iOS 热更新):无需将代码重新打包提交至 AppStore,即可更新客户端的执行代码,即不用下载 app 而自动更新程序。

现状:苹果禁止了 C# 的部分反射操作,禁止 JIT(即时编译,程序运行时创建并运行新代码),不允许逻辑热更新,只允许使用 AssetBundle 进行资源热更新。

注意:2017 年,苹果更新了热更新政策说明,上线后的项目,一旦发现使用热更新,一样会以下架处理。

webp

常见的 Unity 热更新插件

  • sLua:最快的 Lua 插件

  • toLua:由 uLua 发展而来的,第三代 Lua 热更新方案

  • xLua:特性最先进的 Lua 插件

  • lLRuntime:纯 C# 实现的热更新插件

xLua

接触一个新的 Lua 项目时,先要弄懂 Lua 的加载器规则,只有这样,才能弄懂项目的 Lua 执行流程。

为什么要用 Lua 调用 C# 代码?

C# 实现的系统,因为 Lua 可以调用,所以完全可以换成 Lua 实现,因为 Lua 可以即时更改,即时运行,所以游戏的代码逻辑就可以随时修改。

实现和 C# 相同效果的系统,如何实现?

Lua 调用 Unity 的各种 API,从而实现 C# 开发系统同样的效果。

为什么要用 C# 调用 Lua 代码?

Unity 是基于 C# 语言开发的,所有生命周期函数都是基于 C# 实现,xLua 本身是不存在 Unity 的相关生命周期函数的。如果希望 xLua 能够拥有生命周期函数,那么我们可以实现 C# 作为 Unity 原始调用,再使用 C# 调用 Lua 对应的方法。

Tencent/xLua: xLua is a lua programming solution for C# ( Unity, .Net, Mono) , it supports android, ios, windows, linux, osx, etc. 获取 xLua 的代码放置到项目的 Assets/ 下。

24 xLua 基础调用

创建 Assets/Scripts/Rookie/First.cs

C#
using UnityEngine;
using XLua;  // 使用 xLUa
 
public class First : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        // Lua 是解释型语言,所以需要获得 Lua 的解析器
        // XLua 解析器获得
        LuaEnv env = new LuaEnv();
 
        // 解释器运行 Lua 代码,把字符串当成 Lua 代码执行
        env.DoString("print('Hello World!')");
 
        // 关闭 Lua 解析器
        env.Dispose();
    }
}
webp

创建 Assets/Scripts/Rookie/DoString.cs

C#
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using XLua;
 
public class DoString : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        LuaCallCSharpCode();
        LuaReturnData();
    }
 
    // 使用 Lua 调用 C# 代码
    public void LuaCallCSharpCode()
    {
        LuaEnv env = new LuaEnv();
        // Lua 调用 C# 代码(CS.命名空间.类名.方法名(参数))
        env.DoString("CS.UnityEngine.Debug.Log('From lua')");
        env.Dispose();
    }
 
    // Lua 返回值给 C#
    public void LuaReturnData()
    {
        LuaEnv env = new LuaEnv();
        object[] data = env.DoString("return 100, true");
 
        Debug.Log("data[0] = " + data[0]);
        Debug.Log("data[1] = " + data[1]);
 
        env.Dispose();
    }
}
webp

25 xLua 环境控制

创建 Assets/Scripts/Rookie/Loader.cs

C#
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using XLua;
 
// Lua 是脚本语言,编写代码脚本是实现功能最重要的方式
public class Loader : MonoBehaviour
{
    void Start()
    {
        LuaEnv env = new LuaEnv();
        // 对应 test.lua
        // 内置加载器会扫描预制的目录,查找是否存在 test.lua
        // xLua 存在默认加载器,StreamingAssets 目录下可以加载文件
        env.DoString("require('test')");
        env.Dispose();
    }
}

创建 Assets/StreamingAssets/test.lua

lua
print("Hello World!")
webp

要想修改加载 Lua 的目录:

C#
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using XLua;
 
// Lua 是脚本语言,编写代码脚本是实现功能最重要的方式
public class Loader : MonoBehaviour
{
    void Start()
    {
        MyLoader();
    }
 
    public void MyLoader()
    {
        LuaEnv env = new LuaEnv();
        // 自定义加载器,可以加载指定目录下的 lua 文件
        env.AddLoader(ProjectLoader);
        env.DoString("require('test')");
        env.Dispose();
    }
 
    // 自定义加载器
    // 当 Lua 代码执行 require() 函数时,自定义加载器会尝试获得文件的内容
    // 参数:被加载 Lua 文件的路径
    public byte[] ProjectLoader(ref string filepath)
    {
        // filepath 来自于 Lua 的 require(“文件名")
        // 构造路径,才能将 require 加载的文件指向我们想放 Lua 的路径下去
        string path = Application.dataPath;
        path = path.Substring(0, path.Length - 7) + "/DataPath/Lua" + filepath + ".lua";
        Debug.Log("加载 Lua 文件:" + path);
        // 将 Lua 文件读取为数组
        // xLua 的解析环境,会执行我们自定义加载器返回的 Lua 代码
 
        return File.ReadAllBytes(path);
    }
}
webp

全局控制 Lua 加载的路径:

创建 Assets/Tool/xLuaEnv.cs

C#
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using XLua;
 
public class xLuaEnv
{
    #region Singleton
 
    private static xLuaEnv _Instance = null;
 
    // 单例的核心实现
    public static xLuaEnv Instance
    {
        get
        {
            if (_Instance == null)
            {
                _Instance = new xLuaEnv();
            }
 
            return _Instance;
        }
    }
 
    #endregion
 
    #region Create LuaEnv
 
    private LuaEnv _Env;
 
    // 创建单例的时候,Lua 运行环境,会一起被创建
    private xLuaEnv()
    {
        _Env = new LuaEnv();
        _Env.AddLoader(_ProjectLoader);
    }
 
    #endregion
 
    #region Loader
 
    // 创建自定义 Lua 加载器,这样就可以任意订制项目的 Lua 脚本的存储位置
    private byte[] _ProjectLoader(ref string filepath)
    {
        string path = Application.dataPath;
        path = path.Substring(0, path.Length - 7) + "/DataPath/Lua/" + filepath + ".lua";
        Debug.Log("xLuaEnv: Load Lua file: " + path);
 
        if (File.Exists(path))
        {
            return File.ReadAllBytes(path);
        }
        else
        {
            return null;
        }
    }
 
    #endregion
 
    #region Free LuaEnv
 
    public void Free()
    {
        // 释放 LuaEnv,同时也释放单例对象,这样下次调单例对象,会再次产生 Lua 运行环境
        _Env.Dispose();
        _Instance = null;
    }
 
    #endregion
 
    #region Run Lua
 
    public object[] DoString(string code)
    {
        return _Env.DoString(code);
    }
 
    #endregion
}

创建 DataPath/Lua/test2.lua

lua
print("Hello World, I' m test2.lua")

创建 Scripts/Rookie/TestSingleton.cs

C#
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
 
public class TestSingleton : MonoBehaviour
{
    void Start()
    {
        xLuaEnv.Instance.DoString("require('test2')");
    }
 
    void OnDestroy()
    {
        xLuaEnv.Instance.Free();
    }
}
webp

26 xLua 的 Lua 调用 C# 1

创建 Assets/Scripts/C2L/LuaCallStatic.cs

C#
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
 
namespace HX
{
    public static class TestStatic
    {
        public static int ID = 99;
        public static string Name
        {
            get;
            set;
        }
        public static string Output()
        {
            return "static";
        }
        public static void Default(string str = "abc")
        {
            Debug.Log(str);
        }
    }
}
 
public class LuaCallStatic : MonoBehaviour
{
    void Start()
    {
        xLuaEnv.Instance.DoString("require('C2L/LuaCallStatic')");
    }
 
    void OnDestroy()
    {
        xLuaEnv.Instance.Free();
    }
}
 

创建 DataPath/Lua/C2L/LuaCallStatic.lua

lua
-- Lua 调用静态类
 
-- 规则“CS.命名空间.类名称.成员变量”
print(CS.HX.TestStatic.ID)
 
-- 给静态属性赋值
CS.HX.TestStatic.Name = "admin";
print(CS.HX.TestStatic.Name)
 
-- 静态成员方法调用
-- 规则“CS.命名空间.类名.方法名.方法名()”
print(CS.HX.TestStatic.Output())
 
-- 使用默认值
CS.HX.TestStatic.Default();
 
-- 使用 Lua 传递的值
CS.HX.TestStatic.Default("def");
webp

27 xLua 的 Lua 调用 C# 2

创建 Assets/Scripts/C2L/LuaCallObject.cs

C#
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
 
public class Npc
{
    public string Name;
    public int HP
    {
        get;
        set;
    }
    public Npc()
    {
 
    }
    public Npc(string name)
    {
        Name = name;
    }
    public string Output()
    {
        return this.Name;
    }
}
 
public class LuaCallObject : MonoBehaviour
{
    void Start()
    {
        xLuaEnv.Instance.DoString("require('C2L/LuaCallObject')");
    }
 
    void OnDestroy()
    {
        xLuaEnv.Instance.Free();
    }
}

创建 DataPath/Lua/C2L/LuaCallObject.lua

lua
-- Lua 实例化类
-- C# Npc obj = new Npc();
-- 通过调用构造函数创建对象
local obj = CS.Npc()
obj.HP = 100
 
print(obj.HP)
 
local obj1 = CS.Npc("admin")
print(obj1.Name)
 
-- 表方法希望调用表成员变量(表:函数())
print(obj1:Output())
 
-- Lua 实例化 GameObject
-- C# GameObject obj = new GameObject("LuaCreateGO");
local go = CS.UnityEngine.GameObject("LuaCreateGO")
go:AddComponent(typeof(CS.UnityEngine.BoxCollider))
webp

创建 Assets/Scripts/C2L/LuaCallStruct.cs

C#
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
 
public struct TestStruct
{
    public string Name;
    public string Output()
    {
        return Name;
    }
}
 
public class LuaCallStruct : MonoBehaviour
{
    void Start()
    {
        xLuaEnv.Instance.DoString("require('C2L/LuaCallStruct')");
    }
 
    void OnDestroy()
    {
        xLuaEnv.Instance.Free();
    }
}

创建 DataPath/Lua/C2L/LuaCallStruct.lua

lua
-- 和对象调用保持一致
local obj = CS.TestStruct()
 
obj.Name = "admin"
 
print(obj.Name)
webp

创建 Assets/Scripts/C2L/LuaCallEnum.cs

C#
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
 
public enum TestEnum
{
    LoL = 0,
    Dota2
}
 
public class LuaCallEnum : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        xLuaEnv.Instance.DoString("require('C2L/LuaCallEnum')");
    }
 
    void OnDestroy()
    {
        xLuaEnv.Instance.Free();
    }
}
 

创建 DataPath/Lua/C2L/LuaCallEnum.lua

lua
-- C# TestEnum.LOL
-- CS.命名空间.枚举名.枚举值
print(CS.TestEnum.LoL)
print(CS.TestEnum.Dota2)
webp

28 xLua 的 Lua 调用 C# 3

创建 Assets/Scripts/C2L/LuaCallOverload.cs

C#
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
 
public class TestOverload
{
    public static void Test(int id)
    {
        Debug.Log("数字类型:" + id);
    }
    public static void Test(string name)
    {
        Debug.Log("字符串类型:" + name);
    }
    public static void Test(int id, string name)
    {
        Debug.Log("两个数值:" + id + "," + name);
    }
}
 
public class LuaCallOverload : MonoBehaviour
{
    void Start()
    {
        xLuaEnv.Instance.DoString("require('C2L/LuaCallOverload')");
    }
 
    void OnDestroy()
    {
        xLuaEnv.Instance.Free();
    }
}

创建 DataPath/Lua/C2L/LuaCallOverload.lua

lua
CS.TestOverload.Test(99)
CS.TestOverload.Test("admin")
CS.TestOverload.Test(100, "root")
webp

创建 Assets/Scripts/C2L/LuaCallBase.cs

C#
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
 
public class Father
{
    public string Name = "father";
    public void Talk()
    {
        Debug.Log("这是父类中的方法");
    }
    public virtual void Override()
    {
        Debug.Log("这是父类中的虚方法");
    }
}
 
public class Child : Father
{
    public override void Override()
    {
        Debug.Log("这是子类中的重写方法");
    }
}
 
public class LuaCallBase : MonoBehaviour
{
    void Start()
    {
        xLuaEnv.Instance.DoString("require('C2L/LuaCallBase')");
    }
 
    void OnDestroy()
    {
        xLuaEnv.Instance.Free();
    }
}

创建 DataPath/Lua/C2L/LuaCallOverload.lua

lua
-- 调用 Father
local father = CS.Father()
print(father.Name)
father:Override()
 
-- 调用 Child
local child = CS.Child()
print(child.Name)
child:Talk()
child:Override()
webp

创建 Assets/Scripts/C2L/LuaCallExtend.cs

C#
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using XLua;
 
public class TestExtend
{
    public void Output()
    {
        Debug.Log("类本身带的方法");
    }
}
 
// 类扩展,需要给扩展方法编写的静态类添加 [LuaCallCSharp],否则 Lua 无法调用到
[LuaCallCSharp]
public static class MyExtend
{
    public static void Show(this TestExtend obj)
    {
        Debug.Log("类扩展实现的方法");
    }
}
 
public class LuaCallExtend : MonoBehaviour
{
    void Start()
    {
        xLuaEnv.Instance.DoString("require('C2L/LuaCallExtend')");
    }
 
    void OnDestroy()
    {
        xLuaEnv.Instance.Free();
    }
}
 

注意

这是 C# 的扩展方法(Extension Method) 写法,它允许你为已存在的类添加新方法,而无需修改该类的代码。

代码解析:

  1. TestExtend
    • 这是一个普通的 C# 类,定义了一个 Output 方法。
  2. MyExtend 静态类
    • 该类用于存放扩展方法。
    • Show 方法是一个 扩展方法,它的第一个参数 this TestExtend obj 表示该方法是 TestExtend 类的扩展方法。

创建 DataPath/Lua/C2L/LuaCallExtend.lua

lua
-- 获取对象
local obj = CS.TestExtend()
 
obj:Output()
 
obj:Show()
webp

创建 Assets/Scripts/C2L/LuaCallDelegate.cs

C#
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
 
public delegate void DelegateLua();
 
public class TestDelegate
{
    public static DelegateLua Static;
    public DelegateLua Dynamic;
    public static void StaticFunc()
    {
        Debug.Log("C# 静态成员函数");
    }
}
 
public class LuaCallDelegate : MonoBehaviour
{
    void Start()
    {
        xLuaEnv.Instance.DoString("require('C2L/LuaCallDelegate')");
    }
 
    void OnDestroy()
    {
        xLuaEnv.Instance.Free();
    }
}

创建 DataPath/Lua/C2L/LuaCallDelegate.lua

lua
-- C# 给委托赋值
-- TestDelegate.Static = TestDelegate.StaticFunc
-- TestDelegate.Static += TestDelegate.StaticFunc
-- TestDelegate.Static -= TestDelegate.StaticFunc
-- TestDelegate.Static()
 
CS.TestDelegate.Static = CS.TestDelegate.StaticFunc
CS.TestDelegate.Static()
-- Lua 中如果添加了函数到静态委托变量中后,再委托不再使用后,记得释放添加的委托函数
CS.TestDelegate.Static = nil
 
print("-------------------")
 
----------------------------------------------------
 
local func = function()
	print("这是 Lua 的函数")
end
 
-- 覆盖添加委托
CS.TestDelegate.Static = func
-- 加减操作前一定要确定已经添加过回调函数
CS.TestDelegate.Static = CS.TestDelegate.Static + func
CS.TestDelegate.Static = CS.TestDelegate.Static - func
-- 调用以前应确定委托有值
CS.TestDelegate.Static()
 
CS.TestDelegate.Static = nil
 
print("-------------------")
 
-----------------------------------------------------------
 
-- 调用前判定
--	if(CS.TestDelegate.Static ~= nil)
--	then
--		CS.TestDelegate.Static()
--	end
 
-- 根据委托判定赋值方法
--	if(CS.TestDelegate.Static == nil)
--	then
--		CS.TestDelegate.Static = func
--	else
--		CS.TestDelegate.Static = CS.TestDelegate.Static + func
--	end
 
-----------------------------------------------------------
 
local obj = CS.TestDelegate()
obj.Dynamic = func
obj.Dynamic()
 
obj.Dynamic = nil
 
print("-------------------")
webp

创建 Assets/Scripts/C2L/LuaCallEvent.cs

C#
using UnityEngine;
 
public delegate void EventLua();
 
public class TestEvent
{
    public static event EventLua Static;
    public static void StaticFunc()
    {
        Debug.Log("这是静态函数");
    }
    public static void CallStatic()
    {
        if (Static != null)
            Static();
    }
    public event EventLua Dynamic;
    public void CallDynamic()
    {
        if (Dynamic != null)
            Dynamic();
    }
}
 
public class LuaCallEvent : MonoBehaviour
{
    void Start()
    {
        xLuaEnv.Instance.DoString("require('C2L/LuaCallEvent')");
    }
 
    void OnDestroy()
    {
        xLuaEnv.Instance.Free();
    }
}

创建 DataPath/Lua/C2L/LuaCallEvent.lua

lua
-- C# 添加事件 TestEvent.Static += TestEvent.StaticFunc;
 
-- Lua 添加事件
CS.TestEvent.Static("+", CS.TestEvent.StaticFunc)
CS.TestEvent.CallStatic()
CS.TestEvent.Static("-", CS.TestEvent.StaticFunc)
 
-- 添加动态成员变量
local func = function()
    print("来自于 Lua 的回调函数")
end
 
local obj = CS.TestEvent()
obj:Dynamic("+", func)
obj:CallDynamic()
obj:Dynamic("-", func)
webp

29 xLua 的 Lua 调用 C# 4

创建 Assets/Scripts/C2L/LuaCallGenericType.cs

C#
using UnityEngine;
 
public class TestGenericType
{
    public void Output<T>(T data)
    {
        Debug.Log("泛型方法:" + data.ToString());
    }
    public void Output(int data)
    {
        Output<int>(data);
    }
    public void Output(string data)
    {
        Output<string>(data);
    }
}
 
public class LuaCallGenericType : MonoBehaviour
{
    void Start()
    {
        gameObject.AddComponent<BoxCollider>();
        gameObject.AddComponent(typeof(BoxCollider));
        xLuaEnv.Instance.DoString("require('C2L/LuaCallGenericType')");
    }
 
    void OnDestroy()
    {
        xLuaEnv.Instance.Free();
    }
}

创建 DataPath/Lua/C2L/LuaCallGenericType.lua

lua
local obj = CS.TestGenericType()
 
obj:Output(99)
obj:Output("admin")
 
local go = CS.UnityEngine.GameObject("LuaCreate")
go:AddComponent(typeof(CS.UnityEngine.BoxCollider))
webp webp

创建 Assets/Scripts/C2L/LuaCallOutRef.cs

C#
using UnityEngine;
 
public class TestOurRef
{
    public static string Func1()
    {
        return "Func1";
    }
    public static string Func2(string str1, out string str2)
    {
        str2 = "Func2 out";
        return "Func2";
    }
    public static string Func3(string str1, ref string str2)
    {
        str2 = "Func3 Ref";
        return "Func3";
    }
    public static string Func4(ref string str1, string str2)
    {
        str1 = "Func4 Ref";
        return "Func4";
    }
}
 
public class LuaCallOutRef : MonoBehaviour
{
    void Start()
    {
        xLuaEnv.Instance.DoString("require('C2L/LuaCallOutRef')");
    }
 
    void OnDestroy()
    {
        xLuaEnv.Instance.Free();
    }
}

创建 DataPath/Lua/C2L/LuaCallOutRef.lua

lua
local r1 = CS.TestOutRef.Func1()
print(r1)
 
-- C# out 返回的变量,会赋值给 Lua 的第二接收返回值变量
local out2
local r2, out1 = CS.TestOutRef.Func2("admin", out2)
print(r2, out1, out2)
 
-- C# ref 返回的变量,会赋值给 Lua 的第二个接受返回值变量
local ref2
local r3, ref1 = CS.TestOutRef.Func3("root", ref2)
print(r2, ref1, ref2)
 
-- 即使 out ref 作为第一个参数,其结果依然会以 Lua 的多个返回值进行返回
local ref4
local r4, ref3 = CS.TestOutRef.Func4(ref4, "test")
print(r4, ref3, ref4)
webp

31 C# 调用 Lua 方法 1

编辑 Assets/Scripts/Tool/xLuaEnv.lua

C#
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using XLua;
 
public class CsharpCallVariable : MonoBehaviour
{
    void Start()
    {
        xLuaEnv.Instance.DoString("require('L2C/CSharpCallVariable')");
        // LuaEnv 提供了一个成员变量 Global,它可以用于 C# 获取的 Lua 全局变量
        // Global 的数据类型是 C# 实现的 LuaTable,LuaTable 是 xLua 实现的 C# 和 Lua 中表对应的数据结构
        // xLua 会将 Lua 中的全局变量以 Table 的方式全部存储在 Global 中
        // 通过运行环境,导出全局变量,类型是 LuaTable
        LuaTable g = xLuaEnv.Instance.Global;
 
        // 从 Lua 中,将全局变量提取出来
        // 参数:Lua 中全局变量的名称
        // 类型:Lua 中全局变量的名称所对应的类型
        // 返回值:变量的值
        int num = g.Get<int>("num");
        float rate = g.Get<float>("rate");
        bool isWoman = g.Get<bool>("isWoman");
        string name = g.Get<string>("name");
 
        Debug.Log("数字:" + num);
        Debug.Log("浮点数:" + rate);
        Debug.Log("布尔:" + isWoman);
        Debug.Log("字符串:" + name);
 
    }
 
    void OnDestroy()
    {
        xLuaEnv.Instance.Free();
    }
}

创建 Assets/Scripts/L2C/CsharpCallVariable.cs

C#
using UnityEngine;
using XLua;
 
public class CsharpCallVariable : MonoBehaviour
{
    void Start()
    {
        xLuaEnv.Instance.DoString("require('L2C/CSharpCallVariable')");
        // LuaEnv 提供了一个成员变量 Global,它可以用于 C# 获取的 Lua 全局变量
        // Global 的数据类型是 C# 实现的 LuaTable,LuaTable 是 xLua 实现的 C# 和 Lua 中表对应的数据结构
        // xLua 会将 Lua 中的全局变量以 Table 的方式全部存储在 Global 中
        // 通过运行环境,导出全局变量,类型是 LuaTable
        LuaTable g = xLuaEnv.Instance.Global;
 
        // 从 Lua 中,将全局变量提取出来
        // 参数:Lua 中全局变量的名称
        // 类型:Lua 中全局变量的名称所对应的类型
        // 返回值:变量的值
        int num = g.Get<int>("num");
        float rate = g.Get<float>("rate");
        bool isWoman = g.Get<bool>("isWoman");
        string name = g.Get<string>("name");
 
        Debug.Log("数字:" + num);
        Debug.Log("浮点数:" + rate);
        Debug.Log("布尔:" + isWoman);
        Debug.Log("字符串:" + name);
 
    }
 
    void OnDestroy()
    {
        xLuaEnv.Instance.Free();
    }
}

创建 DataPath/Lua/L2C/CsharpCallVariable.lua

lua
num = 100
rate = 99.99
isWoman = false
name = "admin"
webp

32 C# 调用 Lua 方法 2

创建 Assets/Scripts/L2C/CSharpCallFunction.cs

C#
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using XLua;
 
[CSharpCallLua]
public delegate void Func1();
[CSharpCallLua]
public delegate void Func2(string name);
[CSharpCallLua]
public delegate string Func3();
[CSharpCallLua]
public delegate void Func4(out string name, out int id);
 
public class CSharpCallFunction : MonoBehaviour
{
    void Start()
    {
        xLuaEnv.Instance.DoString("require('L2C/CSharpCallFunction')");
 
        LuaTable g = xLuaEnv.Instance.Global;
 
        Func1 func1 = g.Get<Func1>("func1");
        func1();
 
        Func2 func2 = g.Get<Func2>("func2");
        func2("admin");
 
        Func3 func3 = g.Get<Func3>("func3");
        Debug.Log(func3() + ",被 C# 打印");
 
        // 这里是关键
        Func4 func4 = g.Get<Func4>("func4");
 
        string name;
        int id;
        func4(out name, out id);
 
        Debug.Log(name + ", " + id);
    }
 
    void OnDestroy()
    {
        xLuaEnv.Instance.Free();
    }
}
 

创建 DataPath/Lua/L2C/CSharpCallFunction.lua

lua
func1 = function()
    print("这是 Lua 中的 func1")
end
 
func2 = function(name)
    print("这是 Lua 中的 func2,参数是:".. name)
end
 
func3 = function()
    print("这是 Lua 中的 func3")
end
 
func4 = function()
    return "这是 Lua 中的 func4", 100
end

编译一下:

webp

注意

XLua 默认不会支持 out 参数的委托,必须手动标记 [CSharpCallLua]

运行 XLua/Generate Code,然后重启 Unity,让 XLua 重新生成绑定代码。

如果你需要调用多个 Lua 方法,推荐使用接口 来管理,而不是单独获取每个方法。

webp

33 C# 调用 Lua 方法 3

创建 Assets/Scripts/L2C/CSharpCallTable.cs

C#
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using XLua;
 
public delegate void OneStringParams(string name);
public delegate string OneStringReturn();
// 针对结构体调用后添加
public delegate void TransSelf(LuaTable table);
[CSharpCallLua]
public delegate void TransMy(LuaCore table);
 
// Lua 的 table 导出到 C# 的结构体,可以实现 C# 运行时无 GC
[GCOptimize]
public struct LuaCore
{
    public int ID;
    public string Name;
    public bool IsWoman;
 
    public OneStringParams Func1;
    public OneStringReturn Func2;
    public TransMy Func3;
    public TransMy Func4;
}
 
public class CsharpCallTable : MonoBehaviour
{
    void Start()
    {
        xLuaEnv.Instance.DoString("require('L2C/CsharpCallTable')");
        UseLuaTable();
    }
 
    public void UseLuaTable()
    {
        LuaTable g = xLuaEnv.Instance.Global;
 
        // 获取的是全局变量 Core,因为它在 Lua 中是表,所以取出的是 LuaTable
        LuaTable core = g.Get<LuaTable>("Core");
        // 获取 Name
        // 参数:table 中索引名
        // 类型:索引对应值的类型
        Debug.Log(core.Get<string>("Name"));
 
        core.Set<string, string>("Name", "admin");
 
        OneStringParams osp = core.Get<OneStringParams>("Func1");
        osp("admin");
 
        // 调用 Func2(Lua 函数返回字符串)
        OneStringReturn func2 = core.Get<OneStringReturn>("Func2");
        string result = func2();  // 调用 Lua 函数
        Debug.Log("Func2 返回: " + result);
 
        // 调用 Func3(传递 LuaTable 作为 self 参数)
        TransSelf func3 = core.Get<TransSelf>("Func3");
        func3(core); // 传递 core 作为 self
 
        // 相当于“:”调用
        TransSelf ts = core.Get<TransSelf>("Func4");
        ts(core);
    }
 
    void OnDestroy()
    {
        xLuaEnv.Instance.Free();
    }
}
 

创建 DataPath/Lua/L2C/CSharpCallTable.lua

lua
Core = {}
 
Core.ID = 100
Core.Name = "root"
Core.IsWoman = false
 
Core.Func1 = function(name)
    print("这是 Core 表的 Func1 函数,接收到 C# 数据" .. name)
end
 
Core.Func2 = function()
    return "这是 Core 表的 Func2 函数"
end
 
Core.Func3 = function(self)
    print("这是 Core 表的 Func3 函数,Core 表的成员变量 Name 是 " .. self.Name)
end
 
function Core:Func4()
    print("这是 Core 表的 Func4 方法,Core 表的成员变量 Name 是 " .. self.Name)
end

编译一下:

webp webp