0%

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

渲染指定物体

在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资源文件,都是可以使用上面的解包工具打开,可以查看到这些文件中具体包含了哪些资源。

参考

功能

在用Unity开发项目时,每次编写或者修改C#脚本,Unity都会自动检查脚本,并进行自动编译。频繁的自动编译,无疑是会降低开发效率,基于这个出发点,我希望能够关闭自动编译功能。在Unity下有两个设置是和自动编译相关的,就是Preferences界面下的General里面的Auto RefreshScript Changes While Playing。前者是自动刷新,每次刷新的时候就会检查脚本是否需要编译,这时候我们可以把自动刷新关掉,然后使用手动的方式进行刷新。后者主要是控制运行时脚本自动编译的时机,是否在运行时进行自动编译。
wwwwwww
上图中的设置项的位置可能会因Unity的版本不同而有所改变,这里我用的Unity版本是Unity 2020.3.33f1c2 Personal
这里写了一个编辑器脚本,将上面的设置暴露在菜单栏中。

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
using UnityEditor;

public class ScriptOptions
{
//Auto Refresh


//kAutoRefresh has two posible values
//0 = Auto Refresh Disabled
//1 = Auto Refresh Enabled


//This is called when you click on the 'Tools/Auto Refresh' and toggles its value
[MenuItem("Tools/Auto Refresh")]
static void AutoRefreshToggle()
{
var status = EditorPrefs.GetInt("kAutoRefresh");
if (status == 1)
EditorPrefs.SetInt("kAutoRefresh", 0);
else
EditorPrefs.SetInt("kAutoRefresh", 1);
}


//This is called before 'Tools/Auto Refresh' is shown to check the current value
//of kAutoRefresh and update the checkmark
[MenuItem("Tools/Auto Refresh", true)]
static bool AutoRefreshToggleValidation()
{
var status = EditorPrefs.GetInt("kAutoRefresh");
if (status == 1)
Menu.SetChecked("Tools/Auto Refresh", true);
else
Menu.SetChecked("Tools/Auto Refresh", false);
return true;
}


//Script Compilation During Play


//ScriptCompilationDuringPlay has three posible values
//0 = Recompile And Continue Playing
//1 = Recompile After Finished Playing
//2 = Stop Playing And Recompile


//The following methods assing the three possible values to ScriptCompilationDuringPlay
//depending on the option you selected
[MenuItem("Tools/Script Compilation During Play/Recompile And Continue Playing")]
static void ScriptCompilationToggleOption0()
{
EditorPrefs.SetInt("ScriptCompilationDuringPlay", 0);
}


[MenuItem("Tools/Script Compilation During Play/Recompile After Finished Playing")]
static void ScriptCompilationToggleOption1()
{
EditorPrefs.SetInt("ScriptCompilationDuringPlay", 1);
}


[MenuItem("Tools/Script Compilation During Play/Stop Playing And Recompile")]
static void ScriptCompilationToggleOption2()
{
EditorPrefs.SetInt("ScriptCompilationDuringPlay", 2);
}


//This is called before 'Tools/Script Compilation During Play/Recompile And Continue Playing'
//is shown to check for the current value of ScriptCompilationDuringPlay and update the checkmark
[MenuItem("Tools/Script Compilation During Play/Recompile And Continue Playing", true)]
static bool ScriptCompilationValidation()
{
//Here, we uncheck all options before we show them
Menu.SetChecked("Tools/Script Compilation During Play/Recompile And Continue Playing", false);
Menu.SetChecked("Tools/Script Compilation During Play/Recompile After Finished Playing", false);
Menu.SetChecked("Tools/Script Compilation During Play/Stop Playing And Recompile", false);


var status = EditorPrefs.GetInt("ScriptCompilationDuringPlay");


//Here, we put the checkmark on the current value of ScriptCompilationDuringPlay
switch (status)
{
case 0:
Menu.SetChecked("Tools/Script Compilation During Play/Recompile And Continue Playing", true);
break;
case 1:
Menu.SetChecked("Tools/Script Compilation During Play/Recompile After Finished Playing", true);
break;
case 2:
Menu.SetChecked("Tools/Script Compilation During Play/Stop Playing And Recompile", true);
break;
}
return true;
}
}

回到开头的问题,为了解决频繁自动编译的问题,这里我禁用用自动刷新的功能,但是这就导致了我每次都需要手动刷新,如果没有手动刷新,那么修改的代码就不会生效。
所以这里换个思路来解决问题,我们只在运行前和退出运行的时候,进行代码编译,而其他时候都不会进行代码编译。这样上面提到的设置都不用管,仍然可以启用自动刷新功能,但是自动刷新不会进行自动编译。刚好Unity提供是否启用自动编译的接口。所以在不需要任何设置的情况下,只需要将下面的编辑器脚本导入到Unity的Editor目录下,在运行前和运行结束的时候会自动检查代码进行自动编译。

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 UnityEditor;

[InitializeOnLoad]
public class CompilerOptionsEditorScript
{
static CompilerOptionsEditorScript()
{
EditorApplication.playModeStateChanged += PlaymodeChanged;
}

static void PlaymodeChanged(PlayModeStateChange state)
{
//在切换运行模式的时候自动进行脚本编译
if (state == PlayModeStateChange.ExitingPlayMode
|| state == PlayModeStateChange.ExitingEditMode)
{
EditorApplication.UnlockReloadAssemblies();
}

//在运行时或编辑模型下,关闭脚本自动编译,解决频繁编译降低开发效率的问题
if (state == PlayModeStateChange.EnteredPlayMode
|| state == PlayModeStateChange.EnteredEditMode)
{
EditorApplication.LockReloadAssemblies();
}
}
}

参考

How to stop automatic assembly compilation from script

这篇文章讲了如何在Docker上安装GitLab,Jenkins,并且最终Jenkins从GitLab上拉取到项目文件。
我最终的目的是希望能够对Unity项目进行打包部署,所以我开始关联Jenkins和Unity。因为我的Unity是装在物理机上,而Jenkins是装在Docker上,所以Jenkins没办法调取Unity进行打包操作。
所以我后面采用的是GitLab部署在Docker上,而Jenkins和Unity都是安装在物理机上,这样Jenkins通过GitLab的Url拉取项目,然后通过Unity Puglin插件调用Unity进行打包,当然也可以使用windows的批处理命令build.bat的方式。采用Unity Plugin的方式的一个好处就是unity的log会直接显示到Jenkins的log界面上。

其中,虽然Jenkins和GitLab的安装位置不一样,但是他们之间的关联操作和之前的是一样的。这里我们讲一下Jenkins和Unity的关联步骤。
首先建一个Unity项目,然后在项目根目录下创建两个文件build_image_url_link.batmyqrcode.py
build_image_url_link.bat

1
2
3
4
set BASE_PATH=D:\ServicesData\Publish\%1\%2
set BASE_URL=http://192.168.31.186:8080/Publish/%1/%2
python myqrcode.py %BASE_URL%/%3_%4.apk %BASE_PATH%\qrcode.png
echo DESC_INFO:%BASE_URL%/qrcode.png,%BASE_URL%/%3_%4.apk

myqrcode.py

1
2
3
4
5
6
import qrcode
import sys
data = sys.argv[1]
path=sys.argv[2]
img = qrcode.make(data)
img.save(path)

wwwwwww
然后在Editor下创建BuildTools.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
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
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using System;

public class BuildTools
{
public static class BuildTargetName
{
public const string android = "Android";
public const string window = "Window";
}
public static class EnvironmentParam
{
public const string projectName = "--projectName";
public const string productName = "--productName";
public const string version = "--version";
public const string target = "--target";
}
public static bool GetEnvironmentParam(string paramName, out string paramValue)
{
paramValue = null;
string[] args = System.Environment.GetCommandLineArgs();
foreach (var s in args)
{
if (s.Contains(paramName))
{
if (s.Split(':').Length > 1)
{
paramValue = s.Split(':')[1];
return true;
}
}
}
return false;
}

[MenuItem("Build/Build APK")]
public static int BuildApk()
{
try
{
if (!GetEnvironmentParam(EnvironmentParam.productName, out string productName))
{
productName = "productName";
}
if (!GetEnvironmentParam(EnvironmentParam.version, out string bundleVersion))
{
bundleVersion = "bundleVersion";
}
if (!GetEnvironmentParam(EnvironmentParam.projectName, out string projectName))
{
projectName = "projectName";
}
if (!GetEnvironmentParam(EnvironmentParam.target, out string target))
{
target = BuildTargetName.android;
}
string extention = ".exe";
PlayerSettings.productName = productName;
PlayerSettings.bundleVersion = bundleVersion;
BuildPlayerOptions opt = new BuildPlayerOptions();
opt.scenes = new string[] { "Assets/Scenes/SampleScene.unity" };
switch (target)
{
case BuildTargetName.android:
opt.target = BuildTarget.Android;
extention = ".apk";
break;
case BuildTargetName.window:
opt.target = BuildTarget.StandaloneWindows64;
extention = ".exe";
break;
}

opt.locationPathName = $"D:/ServicesData/Publish/{projectName}/{target}/{productName}_{bundleVersion}{extention}";

opt.options = BuildOptions.None;
BuildPipeline.BuildPlayer(opt);
Debug.Log("Build Done!");
}
catch(Exception e)
{
Debug.LogError(e.Message);
return -1;
}
return 0;
}
}

wwwwwww
到这一步Unity项目就建好了,然后将它提交的GitLab上。

创建一个打包项目,关联到GitLab,操作步骤参考上面的链接。进入当前打包项目的配置界面。按下图配置。
wwwwwww
wwwwwww
wwwwwww
wwwwwww
wwwwwww
wwwwwww
wwwwwww
-quit -batchmode -nographics -executeMethod BuildTools.BuildApk --productName:$productName --version:$version --projectName:JenkinsUnityForAndroid --target:$target
wwwwwww
build_image_url_link.bat JenkinsUnityForAndroid %target% %productName% %version%
wwwwwww
DESC_INFO:(.*),(.*)
<img src="\1" height="200" width="200" /> <a href="\2">点击下载</a>

这样就基本配置好Jenkins和Unity项目了。但是上面配置中多了很多奇怪的操作,像什么Python脚本之类的。这是因为我想将打包好的包发布到一个本地服务,然后通过二维码下载。
所以这里我需要搭建一个本地服务,看网上有说用蒲公英什么的来当作发布平台,但是我就想本地局域网用,所以使用我比较熟悉的HFS来冲动服务。
wwwwwww
前面打包代码里面会将好的包放到这个服务路径下面,然后可以通过Url来访问,假设当前打好的包在这个路径下,那么手机输入对应的Url就可以下载了。不过每次都要输入Url很麻烦。所以这里考虑使用二维码。
这里讲了使用qrcodeImage两个python插件来生成二维码,所以首先我们要安装Python,这里Python 3.7.7下载,安装默认安装,并勾选写入环境变量。
然后打开命令行窗口,分别输入pip install qrcodepip install Image两个命令,安装好两个插件。
然后回到Jenkins,设置Jenkins内部的环境变量。
wwwwwww
wwwwwww
这里因为我的python是安装在C:\Users\10524\AppData\Local\Programs\Python\Python37目录下的。都准备好后,就可以开始构建了,下面是最终效果。
wwwwwww

参考

docker+gitlab+jenkins从零搭建自动化部署
Jenkins生成APK链接的二维码
Jenkins构建Python项目提示:‘python‘ 不是内部或外部命令,也不是可运行的程序
Python 3.7.7下载Windows x86-64 executable installer,安装默认安装,并勾选写入环境变量。
HFS下载,搭建简易服务。