0%

撕夜
用心爱你你不懂
十年
时间都去哪儿了
其实你不懂我的心
关于你们之间的故事
假如爱有天意

在使用xlua的过程中,可能需要自定义xlua,比如所Astar寻路算法,如果在lua中实现的话,效率是非常低的,所以可以考虑将实现方法放到C/C++,这样运行效率可以大大提升。
这里参照xlua官方文档,总结了一下。

  • 首先下载xlua源码。

  • 下载完后可以用Unity打开这个项目,跑一下01_Helloworld案例,后面扩展xlua也将在这个案例上修改。

  • 然后添加扩展代码,扩展代码是在和Assets同目录的build目录下。

  • 这里我们沿用官方文档的例子,扩展lua-rapidjson。可以在github上搜索,路径的话就是xpol/lua-rapidjson

  • 下载lua-rapidjson源码

  • 在xlua的build目录下新建文件夹lua-rapidjson,然后在lua-rapidjson文件夹下新建文件夹include和src。在官方文档中说的是新建include和source,其实名字是什么都可以,后面配置的时候对应上就行。

  • 然后将lua-rapidjson源码中的lua-rapidjson/rapidjson/include目录全部拷贝到上面新建的include目录下。

  • 再将lua-rapidjson源码中的lua-rapidjson/src目录全部拷贝到上面新建的src目录下。

  • 上面两步就准备好了扩展的源码。

  • 然后是将上面的源码关联到xlua的cmake上。

  • 在xlua的build目录下找到CMakeLists.txt文件。在这个文件里面配置。配置内容如下。下面这个lua-rapidjson/includeua-rapidjson/src/就是上面建的源文件。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    MARK_AS_ADVANCED(XLUA_PROJECT_DIR)

    # 下面是新增部分
    #begin lua-rapidjson
    set (RAPIDJSON_SRC
    lua-rapidjson/src/Document.cpp
    lua-rapidjson/src/Schema.cpp
    lua-rapidjson/src/rapidjson.cpp
    lua-rapidjson/src/values.cpp
    )
    set_property(
    SOURCE ${RAPIDJSON_SRC}
    APPEND
    PROPERTY COMPILE_DEFINITIONS
    LUA_LIB
    )
    list(APPEND THIRDPART_INC lua-rapidjson/include)
    set (THIRDPART_SRC ${THIRDPART_SRC} ${RAPIDJSON_SRC})
    #end lua-rapidjson
    # 上面是新增部分

    if (NOT LUA_VERSION)
    set(LUA_VERSION "5.3.5")
    endif()
  • 配置好后,就可以尝试重新编译xlua源码。在xlua的build目录下找到make_win_lua54.bat文件,双击,会运行控制台命令,看看如果没有报错就说明成功了。

  • 成功后会导出plugin_lua54,这里就是Unity Plugins下要用到的dll。将生成的Plugins拷贝到Unity下替换原本的Plugins。当然拷贝要关闭Unity才行。

  • 替换好Plugins后,重新打开Unity。还是打开01_Helloworld案例。这里将在01_Helloworld案例中测试新增的lua-rapidjson

  • 首选可以新建一个脚本目录XLuaExt,用来存放Xlua扩展脚本。当然也可以不建,后面的扩展代码直接写在LuaDLL.cs脚本中,不过后面更新xlua时就会被覆盖。

  • XLuaExt目录下新建LuaDLL.Rapidjson.cs,并且在脚本中添加如下内容。这样C#脚本就关联上了xlua中的扩展。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    using System.Runtime.InteropServices;
    namespace XLua.LuaDLL
    {
    public partial class Lua
    {
    [DllImport(LUADLL, CallingConvention = CallingConvention.Cdecl)]
    public static extern int luaopen_rapidjson(System.IntPtr L);

    [MonoPInvokeCallback(typeof(LuaDLL.lua_CSFunction))]
    public static int LoadRapidJson(System.IntPtr L)
    {
    return luaopen_rapidjson(L);
    }
    }
    }
  • 然后打开01_Helloworld案例的Helloworld.cs脚本,修改这个脚本成如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    using UnityEngine;
    using XLua;

    namespace XLuaTest
    {
    public class Helloworld : MonoBehaviour
    {
    // Use this for initialization
    void Start()
    {
    LuaEnv luaenv = new LuaEnv();
    luaenv.AddBuildin("rapidjson", XLua.LuaDLL.Lua.LoadRapidJson);
    luaenv.DoString("CS.UnityEngine.Debug.Log('hello world')");

    luaenv.DoString(@"

    local rapidjson = require('rapidjson')
    local t = rapidjson.decode('{""a"":123}')
    print(t.a)
    t.a = 456
    local s = rapidjson.encode(t)
    print('json', s)

    ");

    luaenv.Dispose();
    }

    // Update is called once per frame
    void Update()
    {

    }
    }
    }
  • 然后运行01_Helloworld案例。可以看到LUA: json {"a":456}打印日志。

  • 创建文件夹TSProjectDemo

    1
    [TSProjectDemo]/
  • 进入文件夹,打开cmd控制台

  • 在控制台输入命令npm init -y,会创建package.json文件,

    1
    2
    [TSProjectDemo]/
    package.json
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    {
    "name": "TSProjectDemo",
    "version": "1.0.0",
    "description": "",
    "main": "index.js",
    "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
    },
    "keywords": [],
    "author": "",
    "license": "ISC"
    }
  • 在控制台可以执行npm run test来执行上面的test脚本

  • 在目录下再创建一个Src子目录,ts脚本后面都放这里面

    1
    2
    3
    [TSProjectDemo]/
    package.json
    [Src]/
  • 在控制台输入tsc --init,会创建tsconfig.json

    1
    2
    3
    4
    [TSProjectDemo]/
    package.json
    tsconfig.json
    [Src]/
  • 修改为

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    {
    "compilerOptions":{
    "target": "ES6",
    "module": "NodeNext",
    "esModuleInterop": true,
    "strict": false,
    "outDir": "output",
    "moduleResolution": "NodeNext"
    },
    "include": [
    // 表示编译Src目录下的所有ts文件
    "./Src/**/*.ts"
    ],
    "exclude": [
    "node_modules"
    ]
    }
  • 进Src目录,创建一个测试脚本main.ts

    1
    2
    3
    4
    5
    [TSProjectDemo]/
    package.json
    tsconfig.json
    [Src]/
    main.ts
    1
    2
    3
    // main.ts
    const message : string = "test log .... "
    console.log(message)
  • 回到根目录,然后运行命令tsc,会编译出js脚本

    1
    2
    3
    4
    5
    6
    7
    [TSProjectDemo]/
    package.json
    tsconfig.json
    [Src]/
    main.ts
    [output]/
    main.js
  • 运行命令node ./output/mian.js可以执行代码

  • 也可以修改package.json中的test脚本,这样就可以使用npm run test运行

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    {
    "name": "TSProjectDemo",
    "version": "1.0.0",
    "description": "",
    "main": "index.js",
    "scripts": {
    "test": "node ./output/mian.js"
    },
    "keywords": [],
    "author": "",
    "license": "ISC"
    }
  • 上面是最简单的ts工程了,但是我们开发的时候可能需要外部库,比如nodejs相关的。

  • 比如require指令,或者fs文件库。这时候我们要安装npm install --save-dev @types/node

  • 安装完成后,脚本里面可以引入fs包,import和require两种方式都可以

    1
    2
    3
    4
    5
    // main.ts
    import * as fs from "fs"
    const fsLib = require("fs")
    const messa : string = "test log .... "
    console.log(messa)
  • 假如我想读写xml文件,可以安装npm install xml2js,xml2js可以用来读写xml,实现js对象和xml数据进行转换

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    // main.ts
    import { rejects } from "assert";
    import * as fs from "fs";
    import { resolve } from "path";
    import * as xml2js from "xml2js";

    function readXmlFile(filePath: string):Promise<any>{
    return new Promise((resolve, rejects)=>{
    fs.readFile(filePath, 'utf-8', (err, data)=>{
    if(err){
    rejects(err);
    }
    else{
    xml2js.parseString(data, (err, result)=>{
    if(err){
    rejects(err);
    }
    else{
    resolve(result);
    }
    });
    }
    });
    });
    }

    function writeXmlFile(filePath:string, data:any):Promise<void>{
    return new Promise((resolve, reject)=>{

    const buildOption : xml2js.BuilderOptions = {
    xmldec:{version:"", encoding:"utf-8",standalone:undefined}
    }

    const builder = new xml2js.Builder(buildOption);
    const xml = builder.buildObject(data);
    fs.writeFile(filePath, xml, (err)=>{
    if(err){
    reject(err);
    }
    else{
    resolve();
    }
    });
    });
    }

    async function main() {
    try{
    // 读xml文件
    const xmlData = await readXmlFile("jj.xml");
    console.log(xmlData);

    // xmlData 是一个js对象,根节点是component, $表示xml属性
    xmlData.component.$.attribute = "new value --- 100"

    // js对象转换为xml
    await writeXmlFile("jj2.xml", xmlData)
    console.log("xml file write")
    }
    catch(err){
    console.error("err: ", err);
    }
    }
    main()
  • 上面的xml2js库是没有代码提示的,我们可以额外安装npm install --save-dev @types/xml2js, 这样在vscode中就可以看到代码提示了

  • 上面安装后,package.json文件会变成

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    {
    "name": "TSProjectDemo",
    "version": "1.0.0",
    "description": "",
    "main": "index.js",
    "scripts": {
    "test": "node ./output/main.js"
    },
    "keywords": [],
    "author": "",
    "license": "ISC",
    "devDependencies": {
    "@types/node": "^20.12.7",
    "@types/xml2js": "^0.4.14"
    },
    "dependencies": {
    "xml2js": "^0.6.2"
    }
    }

基于上面的流程,我们就可以开发一些工具了

渲染指定物体

在SRP里面一个非常重要的函数是ScriptableRenderContext.DrawRenderers,通过它可以帮我们完成整个场景的渲染。但是这个函数用起来很方便,因为它封装了很多细节。但是在游戏UI中经常会用RenderTexture来混合UI和模型,就需要将个别物体渲染到RenderTexture。如果我们只想渲染某一两个物体时,操作起来就比较麻烦,因为首先我们需要将需要渲染的物体ScriptableRenderContext.Cull出来,这个剔除操作是针对整个场景的,必然有额外的消耗。特别是我们已经知道要渲染哪个物体的情况下,我们可能希望跳过剔除这一步。但是跳过剔除,ScriptableRenderContext.DrawRenderers函数就用不了了。

这时候我们可以使用CommandBuffer.DrawRanderer函数,针对单个物体进行渲染。具体操作就是获取所有物体的Render组件、材质,使用CommandBuffer渲染。示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
public class RenderData{
public Renderer renderer;
public Material material;
public float sortingFudge;
public int materialIndex;
}
int Comparison(RenderData a, RenderData b){
if(a.material.renderQueue == b.material.renderQueue){
if(a.sortingFudge == b.sortingFudge){
string aBlendMode = a.material.GetTag("BlendMode", false);
string bBlendMode = b.material.GetTag("BlendMode", false);
if(aBlendMode == bBlendMode)
return a.materialIndex - b.materialIndex;
else
return aBlendMode == "AlphaBlend" ? -1 : 1;
}
else
{
return a.sortingFudge >b.sortingFudge ? -1 : 1;
}
}
else
{
return a.material.renderQueue - b.material.renderQueue;
}
}
List<RenderData> renderList = new List<RenderData>();
List<Renderer> tempRenders = new List<Renderer>();
void CalculateRenderData(GameObject go){
tempRenders.Clear();
renderList.Clear();
go.GetComponentsInChildren<Renderer>(tempRenders);
foreach(var render in tempRenders){
float sortingFudge = 0;
if(render is SkinnedMeshRenderer sRender){
sRender.updateWhenOffscreen = true;
}
else if(render is ParticleSystemRenderer pRender){
sortingFudge = pRender.sortingFudge;
}
var materials = render.sharedMaterials;
int index = -1;
foreach(var material in materials){
index = index + 1;
if(material == null) continue;
RenderData renderData = new RenderData{
renderer = render,
material = material,
materialIndex = index,
sortingFudge = sortingFudge,
};
renderList.Add(renderData);
}
}
}
ScriptableRenderContex context;
Camera camera;
void RenderSingleObject(GameObject go, RenderTexture rt){
CalculateRenderData(go);
Camera.SetupCurrent(camera);
CommandBuffer cmd = CommandBufferPool.Get();
context.SetupCameraProperties(camera);
cmd.SetRenderTarget(rt);
cmd.ClearRenderTarget(true, true, Color.clear);
foreach(var data in renderList){
if(data.material.passCount > 0){
for(int i = 0; i< data.material.passCount; ++i){
cmd.DrawRenderer(data.renderer, data.material, data.materialIndex, i);
}
}
}
context.ExecuteCommandBuffer(cmd);
CommandBufferPool.Release(cmd);
}

这里有几个点需要注意:

  • 首先是Camera.SetupCurrent(camera),如果没有这行代码的话,粒子系统是不会渲染的参考
  • ScriptableRenderContext.DrawRenderers支持SRPBatch,但是其他绘制函数不一定支持,比如这里的CommandBuffer.DrawRanderer,这是当前方法的一个弊端。但是Unity2022版本增加了BatchRendererGroup据说是可以实现SRPBatch,参考这里,但是WebGL目前还不支持。BatchRendererGroup的原理是在SRPBatch上扩展的自定义合批方法。
  • 这里也有对剔除的探讨,但是好像并没有什么有价值的东西,暂且留个入口。
  • 另外,Animator组件和ParticleSystem组件上都带有CullingMode属性,一般来说当物体不可见时,我们希望动画和粒子系统都停止,从而减少性能消耗。CullingMode就是用来控制物体不可见时的行为。当使用ScriptableRenderContext.DrawRenderers绘制时,会自动标记物体是否可见的状态,所以通常设置为Automatic,也就是说不可见时停止,可见时照常运行。但是使用CommandBuffer.DrawRanderer绘制时,并不会标记这些可见状态,所以需要强制设置为Always Simulate,当然有些粒子系统设置为Automatic也能正常运行,这个应该是Unity的bug。
  • 当然,我非要用ScriptableRenderContext.DrawRenderers渲染某一个物体也不是不行,就是要把剔除流程走一遍。如果我们需要将多个物体分别渲染到多个纹理上呢?是不是就的把整个流程走多遍?有一种做法,可以只走一边剔除流程,就可以渲染多个模型到多个贴图上。FilteringSettings.renderingLayerMask这个参数就是可以用来控制本次渲染渲染指定渲染层的模型,我们可以为不同的模型设置不同的层,渲染多张纹理的时候把FilteringSettings.renderingLayerMask设置到对应的层。渲染层总共有32层,也就是最多可分32个独立渲染的物体。我之前想过渲染一次设置一次模型的渲染层,这样看起来就可以渲染无数多个独立渲染的物体,但是实际上这是行不通的,因为整个指令是CommandBuffer缓存执行,也就是说动态修改物体的渲染层是无效的,CommandBuffer指令只知道最后一次设置的渲染层。

源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
// ShaderOpenTools.cs
using UnityEngine;
using UnityEditor;
using UnityEditor.Callbacks;
using System.Diagnostics;
using System.Text;
using UnityEditor.ProjectWindowCallback;
public partial class ShaderOpenTools : EditorWindow
{
const string k_WorkDirectoryKey = "Shader_WorkDirectoryKey";
static string m_keyPath = null;
static string keyPath{
get{
if(m_keyPath == null)
m_keyPath = $"{Application.dataPath}_{k_WorkDirectoryKey}";
return m_keyPath;
}
}
static string workDirectory
{
get
{
string path = EditorPrefs.GetString(keyPath, null);
if(string.IsNullOrEmpty(path))
return Application.dataPath;
return path;
}
set
{
EditorPrefs.SetString(keyPath, value);
}
}
[MenuItem("Tools/ShaderOpenTools")]
static void CreateWindow()
{
Rect rect = new Rect();
if(SceneView.lastActiveSceneView != null)
rect = SceneView.lastActiveSceneView.position;
rect.size = new Vector2(400, 200);
rect.position = rect.position + new Vector2(10, 10);
ShaderOpenTools win = GetWindowWithRect<ShaderOpenTools>(rect, true);
win.titleContent = new GUIContent("ShaderOpenTools Setting");
win.Show();
}

string path = null;
void OnEnable()
{
path = workDirectory;
}
void OnGUI()
{
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField(path);
if(GUILaout.Button("o", GUILayout.Width(30)))
{
string temp = path;
temp = EditorUtility.OpenFolderPanel("select shader project", temp, Application.dataPath);
if(!string.IsNullOrEmpty(temp) && path != temp)
{
path = temp;
workDirectory = path;
}
}
EditorGUILayout.EndHorizontal();

GUILayout.FlexibleSpace();
if(GUILayout.Button("Open Project"))
{
OpenProject();
}
}
}

public partial class ShaderOpenTools
{
static ProcessStartInfo CreateProcessStartInfo(string rootDirectory)
{
ProcessStartInfo start = new ProcessStartInfo("cmd.exe");
start.CreateNoWindow = true;
start.ErrorDialog = true;
start.UseShellExecute = false;
start.WorkingDirectory = rootDirectory;

start.RedirectStandardOutput = true;
start.RedirectStandardError = true;
start.RedirectStandardInput = true;
start.StandardOutputEncoding = Encoding.GetEncoding("GBK");
start.StandardErrorEncoding = Encoding.GetEncoding("GBK");
return start;
}
[OnOpenAsset(-100)]
static bool OnShaderClick(int instanceID, int line)
{
string strFilePath = AssetDatabase.GetAssetPath(EditorUtility.InstanceIDToObject(instanceID));
if(strFilePath.EndsWith(".shader")
|| strFilePath.EndsWith(".hlsl")
|| strFilePath.EndsWith(".cginc"))
{
string rootDirectory = workDirectory;
string strFileName = Path.GetFullPath(strFilePath);
ProcessStartInfo start = CreateProcessStartInfo(rootDirectory);
Process process = Process.Start(start);
using(var sw = process.StandardInput)
{
if(line > 0)
{
sw.WriteLine($"code . && code -g \"{strFileName}\":{line} -r");
}
else
{
sw.WriteLine($"code . && code \"{strFileName}\" -r");
}
}
bool rst = true;
using(var sr = process.StandardError)
{
string error;
do
{
error = sr.ReadLine();
if(!string.IsNullOrEmpty(error))
{
rst = false;
}
}
while(error != null);
}
process.Close();
return rst;
}
return false;
}
static void OpenProject()
{
string rootDirectory = workDirectory;
ProcessStartInfo start = CreateProcessStartInfo(rootDirectory);
Process process = Process.Start(start);
using(var sw = process.StandardInput)
{
sw.WroteLine($"code .");
}
using(var sr = process.StandardError)
{
string error;
do
{
error = sr.ReadLine();
}
while(error!=null);
}
process.Close();
}
}

public partial class ShaderOpenTools
{
[SuppressMessage("Microsft.Performance", "CA1812")]
internal class CreateHLSLAsset : EndNameEditAction
{
public override void Action(int instanceId, string pathName, string resourceFile)
{
FileInfo fileInfo = new FileInfo(pathName);
string fileName = Path.GetFileNameWithoutExtension(pathName);
using(var stream = fileInfo.CreateText())
{
string define = ObjectNames.NicifyVariableName(fileName)
.Replace(" ", "_").ToUpper();
stream.WriteLine($"#ifndef _{define}_INCLUDE");
stream.WriteLine($"#define _{define}_INCLUDE");
stream.WriteLine("\t");
stream.WriteLine($"#endif //_{define}_INCLUDE");
}
AssetDatabase.Refresh();
}
}
[MenuItem("Assets/Create/Shader/HLSL")]
static void CreateHLSL()
{
ProjectWindowUtil.StartNameEditingIfProjectWindowExists(
0,CreateInstance<CreateHLSLAsset>(), "NewHLSL.hlsl", null, null);
}
}

参考

VSCode Command line
VSCode settings
VSCode GlobPattern:可以用于设置VSCode不显示.meta文件,在Settings界面,搜索file:exclude,然后增加**/*.meta

使用配置文件

上面是通过路径打开VSCode文件,更实用的一种方法是直接通过配置文件打开。通过配置文件打开有几个好处,就是可以在同一个VSCode里面打开多个文件路径,并且保存设置。配置文件格式如下,文件名为shaderworkspace.code-workspace

1
2
3
4
5
6
7
8
9
10
11
{
"folder":[
{"path":"folder\\path1"},
{"path":"folder\\path2"}
],
"settings":{
"files.exclude":{
"**/*.meta":true
}
}
}

然后前面打开VSCode工程的命令可以改成:

1
2
3
4
5
6
7
8
if(line > 0)
{
sw.WriteLine($"code shaderworkspace.code-workspace && code -g \"{strFileName}\":{line} -r");
}
else
{
sw.WriteLine($"code shaderworkspace.code-workspace && code \"{strFileName}\" -r");
}

当然,上面命令有效的前提是shaderworkspace.code-workspace文件在当前工作目录下。

接着上一篇Unity资源逆向解析,这里再来谈一谈AssetBundles。

AssetBundles 使用

使用AssetBundles总体上分成两步,第一步是创建AssetBundles资源,第二部是使用AssetBundles资源。

AssetBundles 创建

Unity提供内置的BuildPipeline管线和可编程的Scriptable Build Pipeline,这里仅以内置的BuildPipeline管线为例进行讲解。

首先内置的BuildPipeline管线提供了BuildPipeline.BuildAssetBundles()函数来帮助创建资源,有两种调用方式。

  • 方式一:
    public static AssetBundleManifest BuildAssetBundles(string outputPath, BuildAssetBundleOptions assetBundleOptions, BuildTarget targetPlatform);

  • 方式二:
    public static AssetBundleManifest BuildAssetBundles(string outputPath, AssetBundleBuild[] builds, BuildAssetBundleOptions assetBundleOptions, BuildTarget targetPlatform);

方式一是针对项目中所有被标记为AssetBundles的资源,根据其标记的AssetBundles名称对这些资源进行打包的。
方式二是忽略AssetBundles的资源标记,而是通过纯代码的形式,向BuildAssetBundles传入所需要构建的资源,以及AssetBundles资源名称。

如果想实现自动化构建资源的话,偏向于使用方式二,可以完全通过脚本进行控制。
但是这里为了方便,采用方式一进行构建资源。
因此资源构建代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
using UnityEngine;
using UnityEditor;
using System.IO;
public class CreateAssetBundles
{
[MenuItem("Assets/Build AssetBundles")]
static void BuildAllAssetBundles()
{
string assetBundleDirectory = "./assetBundleDirectory";// 这里传你需要的导出的路径
if(!Directory.Existes(assetBundleDirectory))
Directory.CreateDirectory(assetBundleDirectory);
BuildPipeline.BuildAssetBundles(assetBundleDirectory,BuildAssetBundleOptions.None, BuildTarget.StandaloneWindows);
Debug.Log("build assetbundles done!");
}
}

然后要手动编辑哪些资源需要被打包成AssetBundles,根据官方文档,可以对Assets目录下的文件、以及文件夹进行标记,如果标记的是文件夹,那么相当于标记了文件夹下所有子文件。当然文件的标记优先级高于文件夹的优先级。
这里我的目录结构如下:

1
2
3
4
5
6
7
8
9
10
11
资源                          AssetBundleName         资源依赖
[Assets]
[texture] texture
mogutexture.png
planks.png
stone 2.png
2_ZJZ_zhiwu09.fbx mogu
mogucolor.prefab mogucolor [2_ZJZ_zhiwu09.fbx|mogumaterial.mat]
mogumaterial.mat mogumaterial [MoguShader.shader|mogutexture.png]
MoguShader.shader mogushader
ShaderVar.shadervariants mogushader

上面第一列是资源目录结构,第二列是针对资源的AssetBundles资源名称,第三列表示的是该资源所依赖的其他资源。

一切准备就绪后,在Assets菜单下点击Build AssetBundles开始构建资源。
assetBundleDirectory目录下得到如下资源:

1
2
3
4
5
6
7
8
9
10
11
12
13
[assetBundleDirectory]
assetBundleDirectory
assetBundleDirectory.manifest
texture
texture.manifest
mogu
mogu.manifest
mogucolor
mogucolor.manifest
mogumaterial
mogumaterial.manifest
mogushader
mogushader.manifest

其中以manifest为后缀的文件是给人读的,展示了一些基本信息。加载AssetBundles时只需要读取无后缀的文件就行。而assetBundleDirectory这个资源文件记录了其他所有资源的manifest文件信息。
上面打包出来的资源文件可以使用Unity资源逆向解析中提到的解包工具打开。

AssetBundles 加载

本文加载的基本思路是读取assetBundleDirectory资源文件中的信息,然后去构建一个资源目录表,然后在使用资源时,首先访问资源目录表,看看该资源是否已经加载,如果没有加载的话就对该资源进行加载,加载时也会判断资源依赖,待所有依赖资源以及该资源都加载成功后,触发资源加载成功回调函数。
代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.Networking;
using System;

pulic class ResurceData
{
public enum LoadState
{
Null,
Waiting,
Loading,
Loaded,
}
public bool isReadly = false; // 该资源以及其依赖资源是否都成功加载
public string assetBundleName = string.Empty; // 该资源的assetBundle名称
public int serialId = 0; // 给资源的一个唯一id
public List<string> dependanceName = new List<string>(); // 其直接依赖的资源名称列表
public List<string> parentName = new List<string>(); // 运行时引用了该资源的资源名称列表
public AssetBundle assetBundle = null; // 加载得到的AssetBundle
public LoadState loadState = LoadState.Null; // 加载状态
public referenceCount = 0; // 该资源被外部引用的次数(不包括资源与资源之间的引用)
public bool hasRequest {get{return loadState != LoadState.Null;}} // 是否已经执行过资源下载命令
public bool hasError {get{ return loadState == LoadState.Loaded && assetBundle == null;}} // 该资源是否加载失败
}
[Serializable]
public class OnFinishLoadAssetBundle : UnityEvent<int, bool>{}
[Serializable]
public class OnFinishLoadManifest : UnityEvent<bool>{};
public class LoadAndUnloadRemoteAssets : Singleton<LoadAndUnloadRemoteAssets>
{
private in m_MaxId = 0;
private string m_Uri;

private bool m_Inited = false;
public bool inited => m_Inited;
private UnityWebRequest m_MainfestRequest = null;
AssetBundleManifest m_AssetBundleManifest = null;
AssetBundle m_ManifestAssetBundle = null;

Queue<int> m_WaitToCheck = new Queue<int>();

Dictionary<int, ResourceData> allResource = new Dictionary<int, ResourceData>();
Queue<int> m_WaitToLoad = new Queue<int>();
Dictionary<int, UnityWebRequest> m_UnityWebRequests = new Dictionary<int, UnityWebRequest>();
Dictionary<string, int> m_NameToId = new Dictionary<string, int>();
private List<int> m_TORemoveRequest = new List<int>();

public OnFinishLoadMainfest OnFinishLoadMainfest = new OnFinishLoadMainfest();
public OnFinishLoadManifest OnFinishLoadManifest = new OnFinishLoadManifest();

// url 是`assetBundleDirectory`目录的路径,以http:等开头的形式
public void Init(string url)
{
if(m_ManifestRequest != null) return;
m_Inited = false;
m_Uri = url;
int index = url.LastIndexOf('/');
string manifestName = url.Substring(index + 1);
url = Path.Combine(url, manifestName);
m_ManifestRequest = UnityWebRequestAssetBundle.GetAssetBundle(url, 0);
m_ManifestRequest.SendWebRequest();
}
private int GetMaxId()
{
return ++m_MaxId;
}
public AssetBundle GetAssetBundle(int id)
{
if(allResource.TrayGetValue(id, out var resourceData))
return resourceData.assetBundle;
return null;
}
public void UnloadAllAssets()
{
foreach(var resource in allResource)
{
var resourceData = resource.Value;
if(resourceData.assetBundle)
{
resourceData.assetBundle.Unload(true);
}
resourceData.assetBundle = null;
resourceData.loadState = ResourceData.LoadState.Null;
resourceData.isReady = false;
resourceData.referencCount = 0;
resourceData.parentName.Clear();
}
}
public bool UnloadAsset(int id)
{
// 暂时没时间写这块逻辑,感兴趣的话可以把这部分代码补上。
// ToDo
}
public int LoadAsset(string assetbundleName)
{
if(!m_NameToId.TryGetValue(assetbundleName, out in id))
return -1;
return LoadAssetInternal(assetbundleName, null);
}
private int LoadAssetInternal(string assetbundleName, string parent)
{
m_NameToId.TryGetValue(assetbundleName, out var id);
allResource.TryGetValue(id, out var resourceData);
if(!resourceData.hasRequest)
{
foreach(var dep in resourceData.dependanceName)
LoadAssetInternal(dep, assetbundleName);
m_WaitToLoad.Enqueue(id);
resourceData.loadState = ResourceData.LoadState.Waiting;
}
if(parent == null)
++resourceData.referenceCount;
else
resourceData.parentName.Add(parent);
return id;
}
private void Update()
{
if(!m_Inited)
{
if(m_ManifestRequest != null && m_ManifestRequest.isDone)
{
OnManifestDownloaded(m_ManifestRequest);
m_ManifestRequest.Dispose();
m_ManifestRequest = null;
}
}
else
{
foreach(var request in m_UnityWebRequests)
{
if(request.Value.isDone)
{
if(string.IsNullOrEmpty(request.Value.error))
{
var resource = allResource[request.Key];
resour.loadState = ResourceData.LoadState.Loaded;
resource.assetBundle = DownloadHandlerAssetBundle.GetContent(request.Value);
var isReady = true;
// 这个循环其实被包含在下面的循环逻辑中,可以因此该段代码
foreach(var dep in resource.dependanceName)
{
if(!allResource[m_NameToId[dep]].isReady)
{
isReady = false;
}
}
if(isReady)
{
int curId = request.Key;
m_WaitToCheck.Enqueue(curId);
while(m_WaitToCheck.Count > 0)
{
curId = m_WaitToCheck.Dequeue();
resource = allResource[curId];
if(resource.assetBundel == null)
continue;

isReady = true;
// 判断依赖文件
foreach(var dep in resource.dependanceName)
{
if(!allResource[m_NameToId[dep]].isReady)
{
isReady = false;
}
}
if(isReady)
{
resource.isReady = true;
OnFinishLoadAssetBundle.Invoke(curId, true);
// 往上迭代,检查哪些资源引用了该资源,并更新其状态
foreach(var parent in resource.parentName)
{
m_WaitToCheck.Enqueue(m_NameToId[parent]);
}
}
}
}
}
else
{
allResource[request.Key].loadState = ResourceData.LoadState.Loaded;
OnFinishLoadAssetBundle.Invoke(request.Key, false);
Debug.LogError($"fail to load {allResource[request.Key].assetBundleName}. Because:{request.Value.error}");
}
m_ToRemoveRequest.Add(request.Key);
}
}
foreach(var id in m_ToRemoveRequest)
m_UnityWebRequests.Remove(id);
m_ToRemoveRequest.Clear();

// 暂时设置为最多同时加载4个资源
while(m_UnityWebRequests.Count < 4)
{
if(m_WaitToLoad.Count > 0)
{
var id = m_WaitToLoad.Dequeue();
allResource[id].loadState = ResourceData.LoadState.Loading;
var url = Path.Combine(m_Uri, allResource[id].assetBundleName);
var request = UnityWebRequestAssetBundle.GetAssetBundle(url, 0);
request.SendWebRequest();
m_UnityWebRequest.Add(id, requeset);
}
else
{
break;
}
}
}
}
private void OnManifestDownloaded(UnityWebRequest manifestRequest)
{
AssetBundle bundle = DownloadHandlerAssetBundle.GetContent(manifestRequest);
var assetBundleManifest = bundle.LoadAsset<AssetBundleManifest>("AssetBundleManifest");
m_ManifestAssetBundle = bundle;
if(AssetBundleManifest != null)
{
m_Inited = true;
m_AssetBundleManifest = assetBundleManifest;
var allAssetBundles = assetBundleManifest.GetAllAssetBundles();
foreach(var assbundle in allAssetBundles)
{
var dirctD = assetBundleManifest.GetDirectDependencies(assbundle);

ResourceData resourceData = new ResourceData();
resourceData.assetBundleName = assbundle;
resourceData.dependanceName.AddRange(dirctD);
resourceData.serialId = GetMaxId();
allResource.Add(resourceData.serialId, resourceData);
m_NameToId.Add(assbundle, resourceData.serialId);
}
OnFinishLoadManifest.Invoke(true);
Debug.Log("successfully load AssetBundleManifest");
}
else
{
OnFinishLoadManifest.Invoke(false);
Debug.LogError("fail to load AssetBundleManifest");
}
}
}

//=======================
//使用顺序
//先调用Init函数初始化AssetBundleManifest
//然后调用LoadAsset异步加载,每次调用引用加1
//OnFinishLoadManifest,和OnFinishLoadAssetBundle两个回调监听初始化和加载结果
//加载成功后使用GetAssetBundle获取资源包
//用完后使用UnloadAsset卸载资源包,每次调用引用减1,和LoadAsset配套使用
//=======================

AssetBundles 内存分析

这里以实例化mogucolor.prefab为例。总共有以下几个步骤:

  • 调用Init函数初始化AssetBundleManifest
  • LoadAsset异步加载mogucolor,因为其直接以及间接依赖的资源有texturemogumogumaterialmogushader,所以一共会有5个AssetBundle会被加载
  • 然后调用AssetBundle.LoadAsset<GameObject>("mogucolor),提取其中的mogucolor.prefab资源供Unity后面使用。
  • 最后使用GameObject.Inistatiate()函数将mogucolor.prefab实例化出来。

在Profiler监控窗口中,选中Momory,然后在下方选择Detailed。执行实例化加载流程,然后每一步点击Task Sample Editor进行性能数据采样,你会发现下方有以下5大类:

  • Other
  • Assets
  • Not Saved
  • Builtin Resources
  • Scene Momory

通过记录发现,每一步各个类型数据下都各有变化,以下是变化情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
第一步:调用Init函数初始化`AssetBundleManifest`
Other +1 []
NotSaved +2 [AssetBundle+1, AssetBundleManifest+1]
Assets +0 []
Builtin Resources +0 []
SceenMemory +0 []

第二步:LoadAsset异步加载`mogucolor`
Other +5 []
NotSaved +5 [AssetBundle+5]
Assets +0 []
Builtin Resources +0 []
SceenMemory +0 []

第三步:然后调用`AssetBundle.LoadAsset<GameObject>("mogucolor)`
Other +0 []
NotSaved +0 []
Assets +8 [Shader+1, Texture+1, Mesh+1, Material+1, MeshRenderer+1, GameObject+1, MeshFilter+1]
Builtin Resources +0 []
SceenMemory +0 []

第四步:最后使用`GameObject.Inistatiate()`
Other +1 []
NotSaved +2 [RenderTexture+2, Material+1]
Assets +0 []
Builtin Resources +1 [Shader+1]
SceenMemory +4 [Transform+1, GameObject+1, MeshRenderer+1, MeshFilter+1]

可以发现,第一和第二步,主要是增加了NotSaved里面的AssetBundle。第三步增加的是Assets里面的资源数据。第四步增加的是场景里面的数据,至于NotSaved和Builtin Resources数据也被动增加,主要是因为新入场景的物体可能会导致渲染流程发生改变,所以内置的一些材质或者RenderTexture会有所增加。
至于Builtin Resources里面的数据,从分析上来看主要是GUI相关的的一些内置数据,以及异常Shader等。

AssetBundles 与 Shader

Shader也是一种资源,和模型、贴图资源一样,可以打包进AssetBundle。但是Shader又是一种程序,只不过是运行在GPU上的,因此为了节约性能,需要对Shader进行裁剪,将不需要的变体剔除掉。而我们自己使用AssetBundles对Shader进行打包时,也会进行剔除。以下是官方文档中的原文,介绍了如何根据需要将需要的变体打包。

1
2
3
4
5
6
7
8
Interactions between Shaders and AssetBundles
When you build an AssetBundle, Unity uses information in that bundle to select which shader
variants to compile. This includes information about the scenes
, materials, Graphics Settings, and ShaderVariantCollections in the AssetBundle.

Separate build pipelines compile their own shaders independently of other pipelines. If both a Player build and an Addressables build reference a shader, Unity compiles two separate copies of the shader, one for each pipeline.

This process doesn’t account for any runtime information such as keywords, textures, or changes due to code execution. If you want to include specific shaders in your build, either include a ShaderVariantCollection with the desired shaders, or include the shader in your build manually by adding it to the Always Included Shaders list in your graphics settings.

从文档总结就是,假设我们有一个Shader定义了多个关键字,如果我们只想打包某几个关键字相关的变体时,我们可以采用以下几种方式:

  • 创建与变体相对应的材质,然后将这些材质一起打包到这个Shader的AssetBundle中
  • 创建变体集,如ShaderVar.shadervariants,以shadervariants为后缀的资源,在这个变体集中定义变体,然后将变体集一起打包到这个Shader的AssetBundle中
  • 使用Graphics Settings,(暂时不知道怎么使用)。

有一个很重要的点是,一个Shader可以打包进多个AssetBundle,各个AssetBundle中所生成的变体是相互独立的,最终运行时也是相互独立看待。

Shader变体分析

既然谈到Shader变体,这里就再多说几句,在Unity中Shader变体主要是受Shader关键字控制,当然根据官网描述,一个shader里面拥有多个Pass,也是形成变体的因素。

这里主要是谈一下关键字,首选关键字的增加,对于变体的数量影响很大,最极端的情况是变体的数量是关键字数量的指数级。所以我们需要控制关键字的数量,尽量减少关键字的使用。

假设当前关键字的数量已经是最优的了,我们还可以控制关键字的搭配来控制变体的数量。上一节提到了变体与关键字的关系,就是通过材质和shadervariants等形式,来限定哪些变体生成。

但是目前Unity在关键字上还有一个限制,就是关键字数量不超过256,其中Unity内部已经占用了一部分,所以留给我们用来定义关键字的数量根本够不到256。所以在定义关键字时是需要精打细算的。

在Unity2018版本以及之前的版本,关键字只有global全局关键字,也就是说,所有Shader定义过的关键字数量之和不得超过256,这个限制就非常大,如果使用一些第三方资源,有可能会突破这个限制,最终导致项目崩溃等问题。

Unity2018之后的版本,关键字分为了local和global两种,其中global还是延续了全局关键字的逻辑,也就是所有shader共享的,但是local关键字是从属于当前定义的shader。关键字256的数量限制还在,只不过是local和global两种关键字的总和。怎么理解呢?

举个例子,现在只有两个Shader,Shader1和Shader2,Shader1中定义了100个global关键字,50个local关键字,Shader2中定义了90个global关键字,49个local关键字,这些关键字没有重复的。那么在Shader1看来,gloabl关键字有190个,local关键字有50个,两者之和没有超过256。那么在Shader2看来,gloabl关键字有190个,local关键字有49个,两者之和没有超过256。但是将所有关键字都加起来其实是超过256的。但是这两个Shader的变体组合是有效的。也就是说新的关键字规则将一部分全局关键字的份额分配给了局部关键字,具体来说就是192个全局关键字,和64个局部关键字。

关于关键字的上限,以及因为这个上限导致的问题在这里有很多讨论,其中网友发现问题最多的是使用HDRP时,很容易超出限制,以及使用AssetStore里面的资源也会很容易超出限制。所以网友希望能够直接提高上限,或者给个上限可选项,但是Unity客服表示不太愿意这样做。这个问题提出了好几年了,关键字分为了local和global两种也是基于这个问题给出的一个解决方案。

从程序优化的角度来讲,控制关键字的数量是非常有必要的,因此我们首先需要知道有哪些关键字被使用的了,Unity本身并没有提供这个功能(我目前没看到,并且从上面的讨论链接里面,Unity客服也表示这个很难实现)。但是广大的开发网友开发了一些工具可以用来辅助分析关键字。这里我知道的有两个:

  • Shader Control :Shader Control 是一款功能强大的编辑器插件,让你可以完全控制着色器的编译和关键字的使用以及它们在游戏中的影响(广告是这么说的,我穷没用过)
  • Shader Keywords Tool UNITY3D :免费开源的,用了一下下,可用。

另外这里以及这里详细的介绍了变体相关的东西,包括变体数量的计算,以及如何控制变体的数量。而且Unity还提供scriptable shader variants stripping自定义变体剔除的方法,只要是实现IPreprocessShaders,这个回调接口在打包项目和打包Assetbundle资源时都会调用。

扩展命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
using UnityEngine;
using UnityEditor;

class CreateParentForTransforms : ScriptableObject
{
//[MenuItem("MenuUtility/Create Parent For Selection _p")]
[MenuItem("MenuUtility/Create Parent For Selection")]
static void MenuInsertParent()
{
Transform[] selection = Selection.GetTransforms(
SelectionMode.TopLevel | SelectionMode.Editable);
GameObject newParent = ObjectFactory.CreateGameObject("Parent");
Undo.RegisterCreatedObjectUndo(newParent, "Create Parent For Selection");
foreach (Transform t in selection)
{
Undo.SetTransformParent(t, newParent.transform, "Create Parent For Selection 1");
}
}

// Disable the menu if there is nothing selected
//[MenuItem("MenuUtility/Create Parent For Selection _p", true)]
[MenuItem("MenuUtility/Create Parent For Selection", true)]
static bool ValidateSelection()
{
return Selection.activeTransform != null;
}
}

Package扩展命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEditor;
using UnityEditor.PackageManager.Requests;
using UnityEditor.PackageManager;
using UnityEngine;
using System.Text.RegularExpressions;

namespace Unity.Editor.Extention
{
static class PackageMenuItem
{
static String targetPackage;
static EmbedRequest Request;
static ListRequest LRequest;

[MenuItem("Assets/Package/Embed Installed Package")]
static void EmbedInstalledPackage()
{
if(Selection.assetGUIDs.Length > 0)
{
string path = AssetDatabase.GUIDToAssetPath(Selection.assetGUIDs[0]);
Match match = Regex.Match(path, "^Packages/([^/]+)$");
if(match.Success)
{
string targetPackage = match.Groups[1].Value;
//Debug.LogError(targetPackage);
Embed(targetPackage);
}
}
}
[MenuItem("Assets/Package/Embed Installed Package", validate = true)]
static bool Check_EmbedInstalledPackage()
{
if (Selection.assetGUIDs.Length > 0)
{
string path = AssetDatabase.GUIDToAssetPath(Selection.assetGUIDs[0]);
Match match = Regex.Match(path, "^Packages/([^/]+)$");
return match.Success;
}
return false;
}
static void Embed(string inTarget)
{
// Embed a package in the project
Debug.Log("Embed('" + inTarget + "') called");
Request = Client.Embed(inTarget);
EditorApplication.update += Progress;

}

static void Progress()
{
if (Request.IsCompleted)
{
if (Request.Status == StatusCode.Success)
Debug.Log("Embedded: " + Request.Result.packageId);
else if (Request.Status >= StatusCode.Failure)
Debug.Log(Request.Error.message);

EditorApplication.update -= Progress;
}
}


#region Test
[MenuItem("Assets/Package/Test/Get Package Name", priority = 100)]
static void GetPackageName()
{
// First get the name of an installed package
LRequest = Client.List();
EditorApplication.update += LProgress;
}

static void LProgress()
{
if (LRequest.IsCompleted)
{
if (LRequest.Status == StatusCode.Success)
{
foreach (var package in LRequest.Result)
{
// Only retrieve packages that are currently installed in the
// project (and are neither Built-In nor already Embedded)
if (package.isDirectDependency && package.source
!= PackageSource.BuiltIn && package.source
!= PackageSource.Embedded)
{
targetPackage = package.name;
Debug.LogError(targetPackage);
//break;
}
}

}
else
Debug.Log(LRequest.Error.message);

EditorApplication.update -= LProgress;

//Embed(targetPackage);

}
}
#endregion
}
}

Asset Ripper

支持打开打包后的资源,对于图片等资源可以直接预览,但是Shader只能查看到名字属性等基本信息,里面的代码变体等信息都是假的。
下载链接

UABE(AssetsBundleExtractor)

支持解析打包后的资源,但是好像只能罗列出资源信息,不能像Asset Ripper一样完美的将资源解包出来。但是支持修改打包后的资源,这是其他工具所不具备的。
下载链接
社区

AssetStudio

Asset Ripper一样,支持打开打包后的资源,但是Shader信息展示的更多一点,可以看到变体信息,但是代码块部分仍然看不了。
下载链接

关于Unity Resources

Unity项目运行时可以通过Resources和AssetsBundle方式读取资源。推荐是使用AssetsBundle,但是Resource资源的组织方式影响着包体的大小,所以即便项目中没有使用到Resources资源读取方式,也还是需要有所了解。

  • 首先Resources资源读取方式,需要将资源放置在Assets目录及其子目录下的Resources目录。只有在这个文件夹下的资源才可以使用Resources的方式读取。
  • 打包项目工程时,Resources下的资源会直接打到项目App,如Apk。
  • Resources资源在App中,是存储在resources.assetsresources.assets.resSsharedassets0.assetssharedassets0.assets.resS文件中。
  • 关于*.assets*.assets.resS两个后缀文件,同名文件中的*.assets存储的是序列化文件。而*.assets.resS里面存储的是二进制数据源,如图片、音效等。
  • *.assets文件类似于Prefab,存储着资源的所有序列化信息,以及对二进制数据源的引用,比如说图片,而*.assets.resS里面便是*.assets里面所引用的实际的图片、音效等数据。
  • Resources目录下的资源是打包到resources*中还是打包到sharedassets*文件中,是有一定的规律。未被引用的资源是打包到resources*中,比如新放入的图片还没有使用。而被引用的资源是被打包到sharedassets*,比如被其他Prefab引用的图片。

除了我们自己创建的Resources资源,Unity在打包时还会拷贝一些默认资源,比如内置的着色器,按钮图集等等。这些资源是被统一打包到unity default resources文件中(实际操作是将这个文件直接拷贝到包内,这个资源文件是平台预先就打包好的)。unity default resources资源实际上是*.assets*.assets.resS文件的合体,也就是包含序列化数据和二进制源数据。整个文件有好几兆,但是实际项目中我们可能不会使用这些内置资源,所以无疑会导致包体变大,资源空间浪费。而且在苹果IOS查重时,这个文件还可能被标记。所以可以适当的修改这个文件。

以上这些Resources资源文件,都是可以使用上面的解包工具打开,可以查看到这些文件中具体包含了哪些资源。

参考