资源
- Unity就业课-AB 包 异步加载-热更新开发(一)_哔哩哔哩_bilibili
- Unity3D 就业课-Lua 语言-热更新开发(二)_哔哩哔哩_bilibili
- Unity3D 就业课-XLua 语言-热更新开发(三)_哔哩哔哩_bilibili
- Unity3D 就业课-C# 调用 Lua 方法-热更新开发(四)_哔哩哔哩_bilibili
正文
编辑器开发
01 编辑器开发基础介绍
如此构建项目:
AssetsScenesScriptsMultiResources.cs
TestAResourcesCube.prefab
TestBResourcesSphere.prefab
给 Main Camera 绑一个 MultiResources.cs:
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;
}
}
运行程序,预制体被创建,这说明 Resources/ 是 Unity 中的一个特殊文件夹(存储跟随游戏包的资源目录)
注意
Plugins:需要跨语言调用的代码逻辑代码存储目录,手机 SDK 接入Resources:存储跟随游戏包的资源目录StreamingAssets:只读,存储跟随游戏包的资源目录
02-03 编辑器检视器面板简单扩展 1
注意
用于在 C# 运行时,传递程序中各种元素(类,结构体,变量,方法,枚举,组件)的行为信息的声明标签。一个声明标签是通过放置在它所在应用元素的前面的方括号“[]”中来描述。
写一个 Assets/Scripts/SimpleInspector.cs:
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);
}
}
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");
}
}
创建一个多选的列表:
[Flags]
public enum PlayerLoveColor
{
Green = 1,
Red = 2,
Pink = 4,
}
04-05 编辑器检视器面板深度扩展 1
编辑 Assets/Scripts/Player.cs:
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:
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 的显示面板:
06 编辑器菜单栏扩展
创建 Assets/Editor/Menu.cs:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
// 顶部菜单类
public class Menu {
// 在顶部显示“工具”菜单,下方有“导出 AB 资源包”,点击执行函数
[MenuItem("工具/导出 AB 资源包")]
static void BuildAB()
{
Debug.Log(Application.persistentDataPath);
}
}
07 编辑器场景视窗扩展
创建 Assets/Editor/PopWindow.cs:
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);
}
}
创建一个 Assets/Scripts/NodeManager.cs:
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:
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:
AB 包
08 复习及今天介绍
注意
AB 包和 Resources 的区别
- 存储
- Resources 内部资源存储在游戏的发布包中,
- AB 包存储在独立的文件中(AB 包存储在非特殊目录下时,不在游戏的发布包中)。
- 加载
- Resources 内部资源使用
Resources.Load() - AB 包(可以理解为可下载的
Resources)- 获得 AB 包文件(下载,解压 streaming 拷贝到可写目录)
- 加载步骤 1:通过 AB 包文件路径,加载 AB 包文件
- 加载步骤 2:通过名称(资源,o)加载内部资源
- Resources 内部资源使用
AssetBundle 的定义
AssetBundle 是把一些资源文件,场景文件或二进制文件以某种紧密的方式保存在一起的一些文件。
AssetBundle 内部不能包含 C# 脚本文件,AssetBundle 可以配合 Lua 实现资源和游戏逻辑代码的更新。
AssetBundle 是独立于游戏主包存在的资源存储文件,使用内部资源时,需要单独下载和加载。
热更新
不关闭 Unity 应用的前提,实现游戏资源和代码逻辑的更新。
09 AB 包介绍及导出工具实现
选中若干资源,打包成 AssetBundle(给其命名)。
创建 Assets/Editor/ExportAB.cs:
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:
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 并绑定在场景的某个组件中:
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);
}
}
11 资源热更新演示
导入资源中的 AB 包:ab/old/test 和 ab/New/test
创建 Assets/Scripts/HotUpdate.cs 并导入到场景中:
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);
}
}
Button 绑定 ChangeAB(),后,即可按下按钮更改图片。
12 优化导出工具及依赖加载
编辑 ExportAB.cs,将 AB 包导出至不同的平台:
[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 包文件)
-
加载主 AB 包
-
根据主 AB 包的配置文件,获得我当前需要加载的 AB 所依赖的 AB 们
-
将所有的依赖 AB 们,加载进来
第二步(加载 AB 包文件)
AB 包 = AssetBundle.LoadFromFile(AB 包文件路径)AssetBundle.LoadFromFileSync(AB 包文件路径)
第三步(加载 AB 包内部资源)
-
资源对象 = AB 包对象.LoadAsset<资源类型>(“资源名称”) -
AB 包对象.LoadAssetSync<资源类型>(“资源名称”)
注意!!!:AB 包不能重复加载
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:
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:
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(); // 将所有没有使用过的资源进行回收
}
}
按 Ctrl + 7 即可调用出内存分析界面,在加载 AB 包后,内存使用量会上升。
Lua
15 Lua 环境搭建
安装 LuaForWindows_v5.1.5-52.exe:
这个仓库可以让你在线使用 Jupyter Notebook 进行 Lua 的编写。
16 Lua 变量与数据类型
Lua 注释:
-- Lua 的单行注释
--[[
这是 Lua 的多行注释
]]变量操作:
name = "hxsd"
print(name)hxsd
print(_VERSION)Lua 5.4
print(id)nil
name = nil
print(name)nil
local name = "Unity"
print(type(name))
print(type(type(name)))string
string
print(type(bb))nil
print(type(bb) == "nil")true
print(type(123))
print(type(1.0))number
number
print(type(true))boolean
print(type('hxsd'))string
17 Lua 字符串操作
local str1 = "Abc"
local str2 = 'def'
print(str1 .. str2)
print(#str1)
print(string.upper(str1))
print(string.lower(str1))Abcdef
3
ABC
abc
-- 多行字符串
local str3 = [[
one
two
]]print("1" + "2")3
-- 字符串查找
-- 参数 1:被查找的字符串
-- 参数 2:查找的内容
-- 返回值起始找到的位置和结束找到的位置
print(string.find("abcdefg", "cde"))3 5
-- 字符串反转
print(string.reverse("abcdefg"))gfedcba
print(string.sub("abcdefg", 3))cdefg
print(string.sub("abcdefg", 3, 6))cdef
local data = "abcdefghijkm"
print(string.sub(data, 3, #data - 1))cdefghijk
print(string.format("I'm the %d player, other is %d", 1, 2))I'm the 1 player, other is 2
print(string.rep("abc", 2))abcabc
print(string.char(65))A
print(string.byte("A"))65
-- 字符串替换
-- 参数 1:原始串
-- 参数 2:需要替换的内容
-- 参数 3:替换的内容
-- 返回替换后的字符串及匹配次数
print(string.gsub("abcd", "bc", "**"))a**d 1
print((string.gsub("abcd", "bc", "**")))a**d
local new_str = string.gsub("abcd", "bc", "**")
print(new_str)a**d
18 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
print(2 ^ 3)8.0
print(2 ^ 0.5)1.4142135623731
print(2 ~= 3)true
- Lua 没有
++ - Lua 没有
+=,-=,*=
if(false)
then
print("条件 1 达成")
elseif(true)
then
print("elseif 达成")
else
print("else 达成")
endelseif 达成
if(true)
then
if(true)
then
print("进入第二层 if")
end
end进入第二层 if
local num = 1
while (num < 3)
do
print(num)
num = num + 1
end1
2
19 Lua 语法介绍
local num = 1
repeat
print(num)
num = num + 1
until(num > 5)1
2
3
4
5
local data = {"aa", "bb", "cc", "dd", "ee"}
-- 参数 1:变量 i 的初始值,遍历 Lua 表,使用 1
-- 参数 2:增长到多少
-- [参数 3]:增长步长,默认值是 1
for i = 1, #data, 2
do
print(data[i])
endaa
cc
ee
-- break 会跳出循环,没有 continue
repeat
if(true)
then
print("此处跳出")
break
end
until(true)
print("继续执行")此处跳出
继续执行
-- 迭代器(遍历 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)
endk: 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 函数与表
function func1()
print("这是 func1")
end
func1()这是 func1
local func3 = function(a, b)
print(a + b)
end
func3(5, 7)
func3(5, 7, 9) -- 参数可以多出来12
12
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
function func5()
return 99, 100
end
print(func5())
-- 将多返回值,同时赋值给两个变量
local num1, num2 = func5()
print(num1)
print(num2)99 100
99
100
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:
local config = {}
config.appName = "hxsd"
return configpackage.loaded["test"]true
local config = require("appName")
print(config.appName)hxsd
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 年,苹果更新了热更新政策说明,上线后的项目,一旦发现使用热更新,一样会以下架处理。
常见的 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:
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();
}
}
创建 Assets/Scripts/Rookie/DoString.cs:
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();
}
}
25 xLua 环境控制
创建 Assets/Scripts/Rookie/Loader.cs:
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:
print("Hello World!")
要想修改加载 Lua 的目录:
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);
}
}
全局控制 Lua 加载的路径:
创建 Assets/Tool/xLuaEnv.cs:
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:
print("Hello World, I' m test2.lua")创建 Scripts/Rookie/TestSingleton.cs:
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();
}
}
26 xLua 的 Lua 调用 C# 1
创建 Assets/Scripts/C2L/LuaCallStatic.cs:
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 调用静态类
-- 规则“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");
27 xLua 的 Lua 调用 C# 2
创建 Assets/Scripts/C2L/LuaCallObject.cs:
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 实例化类
-- 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))
创建 Assets/Scripts/C2L/LuaCallStruct.cs:
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:
-- 和对象调用保持一致
local obj = CS.TestStruct()
obj.Name = "admin"
print(obj.Name)
创建 Assets/Scripts/C2L/LuaCallEnum.cs:
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:
-- C# TestEnum.LOL
-- CS.命名空间.枚举名.枚举值
print(CS.TestEnum.LoL)
print(CS.TestEnum.Dota2)
28 xLua 的 Lua 调用 C# 3
创建 Assets/Scripts/C2L/LuaCallOverload.cs:
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:
CS.TestOverload.Test(99)
CS.TestOverload.Test("admin")
CS.TestOverload.Test(100, "root")
创建 Assets/Scripts/C2L/LuaCallBase.cs:
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:
-- 调用 Father
local father = CS.Father()
print(father.Name)
father:Override()
-- 调用 Child
local child = CS.Child()
print(child.Name)
child:Talk()
child:Override()
创建 Assets/Scripts/C2L/LuaCallExtend.cs:
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) 写法,它允许你为已存在的类添加新方法,而无需修改该类的代码。
代码解析:
TestExtend类- 这是一个普通的 C# 类,定义了一个
Output方法。
- 这是一个普通的 C# 类,定义了一个
MyExtend静态类- 该类用于存放扩展方法。
Show方法是一个 扩展方法,它的第一个参数this TestExtend obj表示该方法是TestExtend类的扩展方法。
创建 DataPath/Lua/C2L/LuaCallExtend.lua:
-- 获取对象
local obj = CS.TestExtend()
obj:Output()
obj:Show()
创建 Assets/Scripts/C2L/LuaCallDelegate.cs:
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:
-- 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("-------------------")
创建 Assets/Scripts/C2L/LuaCallEvent.cs:
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:
-- 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)
29 xLua 的 Lua 调用 C# 4
创建 Assets/Scripts/C2L/LuaCallGenericType.cs:
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:
local obj = CS.TestGenericType()
obj:Output(99)
obj:Output("admin")
local go = CS.UnityEngine.GameObject("LuaCreate")
go:AddComponent(typeof(CS.UnityEngine.BoxCollider))
创建 Assets/Scripts/C2L/LuaCallOutRef.cs:
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:
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)
31 C# 调用 Lua 方法 1
编辑 Assets/Scripts/Tool/xLuaEnv.lua:
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:
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:
num = 100
rate = 99.99
isWoman = false
name = "admin"
32 C# 调用 Lua 方法 2
创建 Assets/Scripts/L2C/CSharpCallFunction.cs:
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:
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编译一下:
注意
XLua 默认不会支持 out 参数的委托,必须手动标记 [CSharpCallLua]。
运行 XLua/Generate Code,然后重启 Unity,让 XLua 重新生成绑定代码。
如果你需要调用多个 Lua 方法,推荐使用接口 来管理,而不是单独获取每个方法。
33 C# 调用 Lua 方法 3
创建 Assets/Scripts/L2C/CSharpCallTable.cs:
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:
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编译一下:
