浅谈多平台SDK下的管理及其自制工具

前言

        我最近的项目真是太麻烦了,本身项目在游戏开发方面倒是还好,但是项目接入其他平台的sdk导致了开发麻烦。不过,这个经历也算好,我至少也算是可以说自己富有多平台sdk管控和协作经验(开个玩笑( ▽ ))。

正文

        一个项目要接入多个平台的SDK,我认为最大的挑战就是防止SDK之间互相冲突。因为Unity在载入程序集时有一个选项————Define Constraints。所以我打算用宏定义来对不同的平台进行处理。一个平台对应一个特定宏,只有这个宏存在的时候,才会载入这个程序集。然而这个方法会出现三个问题。

  1. 宏之间的切换很麻烦,我们先删除旧的宏然后才在加入新的宏。
  2. 给对应SDK的程序集加入宏也很麻烦,我们需要遍历所有程序集,然后一个一个的加入宏约束。
  3. 对于哪些不被程序集包裹的SDK,这个方案就无能为力了。

对于第三点,我自己还没找到好的方案。我现在也只能试着将这样的SDK手动添加程序集包裹。而关于第一点和第二点,我们可以分别写一个脚本工具来解决。

宏切换工具

        我的宏切换工具分成了两个部分,一个是宏定义配置,一个是切换工具。宏定义配置是用来根据你的需求配置不同平台的下的宏。切换工具根据你的配置信息来生成对应切换逻辑。因为Unity原生不支持字典,所以我其实还写了一个宏定义配置的Inspector修改代码。这个代码我也会放到文章内的。

        宏定义配置是按照构造+发布平台来进行区分。这里我举个例子。在开发小游戏的时候,你会发现他们的构造平台都是WebGL。但是发布平台却各不一样,比如微信小游戏,抖音小游戏。而因为发布平台的不同,我们自然要加载不同平台的SDK,因此我将宏定义配置是按照构造+发布平台来进行区分。代码如下:

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
using System;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;

namespace SelfEditor
{
[CreateAssetMenu(fileName = "PlatformMacroSwitcherConfig", menuName = "平台宏设定")]
public class PlatformMacroSwitcherConfig : ScriptableObject
{
[Serializable]
public class PlatformMacroSwitcherConfigData
{
[InspectorName("构建平台")]
public BuildTarget buildPlatform;

[InspectorName("发布平台")]
public string publishPlatform = "Defalut";

public List<string> marcoes;
}

public List<PlatformMacroSwitcherConfigData> config;

[InspectorName("额外宏,防止因宏重名而造成清除")]
public List<string> extraMarco;
}
}

为了防止发布平台相同,我做了额外的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
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
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;

namespace SelfEditor
{
[CustomEditor(typeof(PlatformMacroSwitcherConfig))]
public class PlatformMacroSwitcherConfigEditor : Editor
{
private PlatformMacroSwitcherConfig config;

private BuildTarget curBuildTarget;

private bool originType;

private void OnEnable()
{
config = (PlatformMacroSwitcherConfig)target;

curBuildTarget = EditorUserBuildSettings.activeBuildTarget;
}

public override void OnInspectorGUI()
{
serializedObject.Update();

originType = EditorGUILayout.Toggle("展示Unity默认效果", originType);

if (originType)
{
EditorGUILayout.PropertyField(serializedObject.FindProperty("config"));
}
else
{
ShowSelfSetting();
}

EditorGUILayout.PropertyField(serializedObject.FindProperty("extraMarco"));

serializedObject.ApplyModifiedProperties();
}

private void ShowSelfSetting()
{
curBuildTarget = (BuildTarget)EditorGUILayout.EnumPopup(curBuildTarget);

HashSet<string> curShowInfo = new HashSet<string>();

var configObj = serializedObject.FindProperty("config");

GUILayout.BeginHorizontal();

configObj.isExpanded = EditorGUILayout.Foldout(configObj.isExpanded, configObj.displayName, true);
int preArraySize = configObj.arraySize;
configObj.arraySize = EditorGUILayout.IntField(configObj.arraySize);
GUILayout.EndHorizontal();

if (configObj.isExpanded)
{
EditorGUI.indentLevel++;
int size = 0;
int selectInd = configObj.arraySize - 1;
for (int i = 0; i < configObj.arraySize; ++i)
{
var child = configObj.GetArrayElementAtIndex(i);
if (child.FindPropertyRelative("buildPlatform").enumValueFlag == (int)curBuildTarget)
{
EditorGUILayout.PropertyField(child);
if (child.isExpanded)
selectInd = i;
size++;
}
}
if (size == 0)
EditorGUILayout.LabelField("空列表");
GUILayout.BeginHorizontal();
if (GUILayout.Button("+",GUILayout.MaxWidth(25)))
{
configObj.InsertArrayElementAtIndex(configObj.arraySize);
}
if (GUILayout.Button("-", GUILayout.MaxWidth(25)))
{
configObj.DeleteArrayElementAtIndex(selectInd);
}
GUILayout.EndHorizontal();
EditorGUI.indentLevel--;
}

for (int i = 0; config.config != null && i < config.config.Count; ++i)
{
if (config.config[i].buildPlatform == curBuildTarget)
{
if (curShowInfo.Contains(config.config[i].publishPlatform))
{
Debug.LogError($"第{i}个设定中的平台名字重复了,以进行了强制修改。");
while (curShowInfo.Contains(config.config[i].publishPlatform))
{
config.config[i].publishPlatform += "-副本";
}
curShowInfo.Add(config.config[i].publishPlatform);
}
else curShowInfo.Add(config.config[i].publishPlatform);
}
}

int lastInd = 0;
if(config.config != null)
{
lastInd = config.config.Count;
}

string defalutPublishName = "Defalut";

while (curShowInfo.Contains(defalutPublishName))
{
defalutPublishName += "-副本";
}

// 对添加的进行修改
for(int i = lastInd; i < configObj.arraySize; ++i)
{
var child = configObj.GetArrayElementAtIndex(i);
child.FindPropertyRelative("buildPlatform").enumValueFlag = (int)curBuildTarget;

child.FindPropertyRelative("publishPlatform").stringValue = defalutPublishName;

defalutPublishName += "-副本";
}
}
}
}

最后就是切换工具,其代码如下:

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
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using UnityEditor;
using UnityEngine;

namespace SelfEditor
{
public class PlatformMacroSwitcher : EditorWindow
{
private static string DefalutFoldUrl = "Assets/Editor/MacroSwitch";

[MenuItem("Tools/平台宏切换")]
public static void ShowExample()
{
PlatformMacroSwitcher wnd = GetWindow<PlatformMacroSwitcher>();
wnd.titleContent = new GUIContent("PlatformMacroSwitcher");
}

private PlatformMacroSwitcherConfig config;

private BuildTarget curBuildTarget;

private string[] switchSelects;

private int curSwitchSelects;

private void OnEnable()
{
// 获取到之前加载的配置文件地址
var url = PlayerPrefs.GetString("PlatformMacroSwitcherConfigURL");
if(string.IsNullOrEmpty(url))
{
url = DefalutFoldUrl + "/PlatformMacroSwitcherConfig.asset";
}
// 从对应的文件位置中加载,如果没有那就直接创建
config = AssetDatabase.LoadAssetAtPath<PlatformMacroSwitcherConfig>(url);
if(config == null)
{
PlayerPrefs.DeleteKey("PlatformMacroSwitcherConfigURL");
config = new PlatformMacroSwitcherConfig();
config.name = "PlatformMacroSwitcherConfig";
if(!AssetDatabase.IsValidFolder(DefalutFoldUrl))
{
Directory.CreateDirectory(Application.dataPath + "Editor/MacroSwitch");
}
AssetDatabase.CreateAsset(config, DefalutFoldUrl + "/PlatformMacroSwitcherConfig.asset");
Debug.Log("对应配置已生成在Editor/MacroSwitch文件夹下,其名为PlatformMacroSwitcherConfig");
}

curBuildTarget = EditorUserBuildSettings.activeBuildTarget;

ChangeSwitchSelects();
}

private void OnGUI()
{
bool needChange = false;
EditorGUILayout.BeginHorizontal();
var changeConfig = EditorGUILayout.ObjectField(config,typeof(PlatformMacroSwitcherConfig),false) as PlatformMacroSwitcherConfig;

if (changeConfig != config)
{
config = changeConfig;
needChange = true;
SetCacheInfo("PlatformMacroSwitcherConfigURL", AssetDatabase.GetAssetPath(config));
Debug.Log(AssetDatabase.GetAssetPath(config));
}

if(GUILayout.Button("reload"))
{
ChangeSwitchSelects();
}

EditorGUILayout.EndHorizontal();

EditorGUILayout.BeginHorizontal();
// 显示当前构造平台
BuildTarget changeGroup = (BuildTarget)EditorGUILayout.EnumPopup(curBuildTarget);

// 切换构造
if(changeGroup != curBuildTarget)
{
curBuildTarget = changeGroup;
EditorUserBuildSettings.SwitchActiveBuildTarget(BuildPipeline.GetBuildTargetGroup(curBuildTarget), curBuildTarget);
needChange = true;
}

if (needChange)
ChangeSwitchSelects();

if (switchSelects != null && switchSelects.Length > 0)
{
curSwitchSelects = EditorGUILayout.Popup(curSwitchSelects, switchSelects.ToArray());

EditorGUILayout.EndHorizontal();

EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("切换宏"))
{
ChangeMarco(false);
}
if (GUILayout.Button("清除宏"))
{
ChangeMarco(true);
}
EditorGUILayout.EndHorizontal();
}
else
{
EditorGUILayout.LabelField("无相应宏设定");
EditorGUILayout.EndHorizontal();
}
}

private void ChangeSwitchSelects()
{
curSwitchSelects = 0;
// 缓存中是否有对应的设置缓存
var selectPlatformName = PlayerPrefs.GetString("PlatformMacroSwitcher" + curBuildTarget.ToString());
var tmp = new List<string>();

if(config.config != null)
{
for (int i = 0; i < config.config.Count; ++i)
{
var curInfo = config.config[i];
if (curInfo.buildPlatform == curBuildTarget)
{
tmp.Add(curInfo.publishPlatform);
if (curInfo.publishPlatform.Equals(selectPlatformName))
{
curSwitchSelects = i;
}
}
}
switchSelects = tmp.ToArray();
}
}

private void ChangeMarco(bool isClear)
{
var curGroup = BuildPipeline.GetBuildTargetGroup(curBuildTarget);
string defines = PlayerSettings.GetScriptingDefineSymbolsForGroup(curGroup);

HashSet<string> record = new HashSet<string>(defines.Split(';'));

var selectPlatform = switchSelects[curSwitchSelects];

// 先遍历一遍移除所有记录的宏
foreach (var i in config.config)
{
foreach (var j in i.marcoes)
{
record.Remove(j);
}
}

if(!isClear)
{
// 将这次的宏添加进去
foreach (var i in config.config)
{
if (i.buildPlatform == curBuildTarget && i.publishPlatform.Equals(selectPlatform))
{
foreach (var j in i.marcoes)
{
record.Add(j);
}
break;
}
}
}

foreach(var i in config.extraMarco)
{
record.Add(i);
}
StringBuilder builder = new StringBuilder();
foreach (var i in record)
{
builder.Append(i);
builder.Append(";");
}
builder.Remove(builder.Length - 1, 1);
PlayerSettings.SetScriptingDefineSymbolsForGroup(curGroup, builder.ToString());

SetCacheInfo("PlatformMacroSwitcher" + curBuildTarget.ToString(), selectPlatform);
}

private void SetCacheInfo(string key,string val)
{
PlayerPrefs.SetString(key, val);
PlayerPrefs.Save();
}
}
}

缺陷

        现在工具还是存在缺陷。为了防止无意义的宏,所以我在做宏添加的时候,我先将记录中所有的宏清除后再添加。但是有部分插件需要的宏和你设定的名称一样,那就会存在问题。关于这点,我现在还没想到好办法去做规避,我只提供了一个额外的参数,当进行清除后会额外进行将这些宏添加回去。且它并不告知你现在使用那个平台的宏。这是因为我觉得宏本身会由人为进行更改,额外做这样的操作意义不大。当然如果你需要可以也可以加上去做持久化处理或者直接保存在配置上也可以的。

程序集设定工具

        这个工具的想法很简单就是给定文件夹位置,然后查询文件夹全部的Assembly Definition,最后给他们添加上给定约束。为了泛用性,我并没有将这个工具和前面的宏定义配置工具做联动。其代码如下:

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
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEditor;
using UnityEngine;

namespace SelfEditor
{
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEditor;
using UnityEditor.Compilation;
using UnityEngine;

namespace EditorTools
{
/// <summary>
/// 批量处理 Assembly Definition 文件的编辑器工具。
/// 支持:
/// 1. 批量添加宏约束(defineConstraints)
/// 2. 批量替换平台(includePlatforms / excludePlatforms)
/// </summary>
public class BatchAsmdefTool : EditorWindow
{
// ---------- 共享状态 ----------
private readonly List<string> _targetFolders = new List<string>();
private List<string> _previewPaths = new List<string>();
private Vector2 _folderScrollPos;
private Vector2 _previewScrollPos;

// ---------- Tab ----------
private int _tab;
private readonly string[] _tabNames = { "宏约束", "平台" };

// ---------- Tab 1:宏约束 ----------
private string _defineInput = "";
private bool isOverDefine;

// ---------- Tab 2:平台 ----------
private int _platformMode; // 0 = include, 1 = exclude
private readonly string[] _platformModeNames = { "只在这些平台编译 (Include)", "排除这些平台 (Exclude)" };
private readonly Dictionary<string, bool> _platformSelections = new Dictionary<string, bool>();
private Vector2 _platformScrollPos;

/// <summary>asmdef 支持的平台名称(字符串直接写入 JSON)。</summary>
private string[] allPlatforms;

[MenuItem("Tools/Assembly Definition/批量工具")]
private static void OpenWindow()
{
var window = GetWindow<BatchAsmdefTool>("Assembly Definition 批量工具");
window.minSize = new Vector2(500, 600);
window.Show();
}

private void OnEnable()
{
AssemblyDefinitionPlatform[] platforms = CompilationPipeline.GetAssemblyDefinitionPlatforms();
allPlatforms = new string[platforms.Length];
for(int i = 0;i < allPlatforms.Length; ++i)
{
allPlatforms[i] = platforms[i].Name;
}
foreach (var p in allPlatforms)
{
if (!_platformSelections.ContainsKey(p))
_platformSelections[p] = false;
}
}

// =====================================================================
// 主 GUI
// =====================================================================

private void OnGUI()
{
EditorGUILayout.Space(6);

// ---------- 目标文件夹 ----------
EditorGUILayout.LabelField("目标文件夹", EditorStyles.boldLabel);
DrawFolderSection();

EditorGUILayout.Space(6);

// ---------- 预览 ----------
if (GUILayout.Button("扫描匹配的 asmdef 文件", GUILayout.Height(26)))
{
_previewPaths = FindAllAsmdefs(_targetFolders);
}

if (_previewPaths.Count > 0)
{
EditorGUILayout.LabelField($"找到 {_previewPaths.Count} 个 .asmdef 文件:", EditorStyles.boldLabel);
_previewScrollPos = EditorGUILayout.BeginScrollView(_previewScrollPos, GUILayout.Height(80));
foreach (var p in _previewPaths)
EditorGUILayout.LabelField(p, EditorStyles.miniLabel);
EditorGUILayout.EndScrollView();
}

EditorGUILayout.Space(6);

// ---------- Tab ----------
_tab = GUILayout.Toolbar(_tab, _tabNames, GUILayout.Height(24));
EditorGUILayout.Space(4);

switch (_tab)
{
case 0: DrawDefineTab(); break;
case 1: DrawPlatformTab(); break;
}
}

// =====================================================================
// 文件夹选择区
// =====================================================================

private void DrawFolderSection()
{
// 已添加的文件夹列表
if (_targetFolders.Count == 0)
{
EditorGUILayout.HelpBox("尚未添加文件夹,点击下方按钮选择。", MessageType.Info);
}
else
{
_folderScrollPos = EditorGUILayout.BeginScrollView(
_folderScrollPos, GUILayout.MaxHeight(80), GUILayout.MinHeight(20));

for (int i = _targetFolders.Count - 1; i >= 0; i--)
{
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField(_targetFolders[i], EditorStyles.miniLabel);
if (GUILayout.Button("×", GUILayout.Width(20)))
{
_targetFolders.RemoveAt(i);
_previewPaths.Clear();
}
EditorGUILayout.EndHorizontal();
}

EditorGUILayout.EndScrollView();
}

// 操作按钮
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("添加文件夹...", GUILayout.Width(120)))
{
string picked = EditorUtility.OpenFolderPanel(
"选择包含 Assembly Definition 的文件夹",
Application.dataPath, "");
if (!string.IsNullOrEmpty(picked))
{
TryAddFolder(picked);
_previewPaths.Clear();
}
}
if (_targetFolders.Count > 0 && GUILayout.Button("清空", GUILayout.Width(60)))
{
_targetFolders.Clear();
_previewPaths.Clear();
}
EditorGUILayout.EndHorizontal();
}

/// <summary>把系统绝对路径转成 Unity 相对路径(如 Assets/Scripts)。</summary>
private void TryAddFolder(string absolutePath)
{
if (string.IsNullOrEmpty(absolutePath)) return;

string path = absolutePath.Replace("\\", "/").TrimEnd('/');
string dataPath = Application.dataPath.Replace("\\", "/");

if (path == dataPath)
{
path = "Assets";
}
else if (path.StartsWith(dataPath + "/"))
{
path = "Assets" + path.Substring(dataPath.Length);
}

if (!AssetDatabase.IsValidFolder(path))
{
EditorUtility.DisplayDialog("提示","选择的文件夹不在当前 Unity 工程的 Assets 目录下:\n" + absolutePath,"确定");
return;
}

if (!_targetFolders.Contains(path))
_targetFolders.Add(path);
}

// =====================================================================
// Tab 1:宏约束
// =====================================================================

private void DrawDefineTab()
{
EditorGUILayout.LabelField("宏约束(每行一个,或用逗号/分号分隔)", EditorStyles.boldLabel);
_defineInput = EditorGUILayout.TextArea(_defineInput, GUILayout.Height(70));
isOverDefine = EditorGUILayout.Toggle("是否要覆写", isOverDefine);
EditorGUILayout.HelpBox(
"如果是追加,这些宏会被追加到每个 asmdef 的 defineConstraints 中(已存在的不会重复添加)。\n" +
"多个宏之间是「与」的关系:全部满足才会编译该程序集。",
MessageType.Info);

EditorGUILayout.Space(6);

GUI.enabled = _previewPaths.Count > 0 && !string.IsNullOrWhiteSpace(_defineInput);
if (GUILayout.Button("批量添加宏约束", GUILayout.Height(32)))
{
ExecuteAddDefines();
}
GUI.enabled = true;
}

private void ExecuteAddDefines()
{
string[] defines = ParseDefines(_defineInput);
if (defines.Length == 0)
{
EditorUtility.DisplayDialog("提示", "请输入至少一个宏符号。", "确定");
return;
}

int modified = 0, skipped = 0;
var modifiedPaths = new List<string>();

foreach (string path in _previewPaths)
{
bool changed = ModifyAsmdef(path, data =>
{
bool c = false;
var existing = new HashSet<string>(defines);
if (isOverDefine)
{
data.defineConstraints = existing.ToArray();
c = true;
}
else
{
if(data.defineConstraints != null)
{
foreach (var d in data.defineConstraints)
{
existing.Add(d);
}
}
c = data.defineConstraints == null || data.defineConstraints.Length < existing.Count;
}
if (c)
data.defineConstraints = existing.ToArray();
return c;
});

if (changed) { modified++; modifiedPaths.Add(path); }
else skipped++;
}

AssetDatabase.Refresh();

string msg = $"处理完成。\n\n修改:{modified} 个\n跳过(已存在或无变化):{skipped} 个";
if (modifiedPaths.Count > 0)
{
msg += "\n\n修改的文件:\n" + string.Join("\n", modifiedPaths);
Debug.Log($"[BatchAsmdefTool] 批量添加宏约束完成。\n" + msg);
}
EditorUtility.DisplayDialog("批量添加宏约束", msg, "确定");
}

// =====================================================================
// Tab 2:平台
// =====================================================================

private void DrawPlatformTab()
{
EditorGUILayout.LabelField("操作模式", EditorStyles.boldLabel);
_platformMode = GUILayout.Toolbar(_platformMode, _platformModeNames);

EditorGUILayout.Space(6);

EditorGUILayout.LabelField("选择平台(勾选)", EditorStyles.boldLabel);

EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("全选", GUILayout.Width(50)))
SetAllPlatforms(true);
if (GUILayout.Button("全不选", GUILayout.Width(60)))
SetAllPlatforms(false);
EditorGUILayout.EndHorizontal();

// 三列布局
_platformScrollPos = EditorGUILayout.BeginScrollView(_platformScrollPos, GUILayout.Height(160));
const int columns = 3;
for (int i = 0; i < allPlatforms.Length; i += columns)
{
EditorGUILayout.BeginHorizontal();
for (int j = 0; j < columns && i + j < allPlatforms.Length; j++)
{
string p = allPlatforms[i + j];
_platformSelections[p] = EditorGUILayout.ToggleLeft(p, _platformSelections[p], GUILayout.Width(150));
}
EditorGUILayout.EndHorizontal();
}
EditorGUILayout.EndScrollView();

EditorGUILayout.Space(6);

GUI.enabled = _previewPaths.Count > 0;
if (GUILayout.Button("批量替换平台设置", GUILayout.Height(32)))
{
ExecuteReplacePlatforms();
}
GUI.enabled = true;
}

private void SetAllPlatforms(bool value)
{
foreach (var key in allPlatforms)
_platformSelections[key] = value;
}

private void ExecuteReplacePlatforms()
{
string[] selected = allPlatforms
.Where(p => _platformSelections[p])
.ToArray();

bool isInclude = _platformMode == 0;
string modeName = isInclude ? "Include(白名单)" : "Exclude(黑名单)";

if (selected.Length == 0)
{
if (!EditorUtility.DisplayDialog("确认",
"没有勾选任何平台,这会把所有相关平台字段清空,让程序集在所有平台生效。继续?",
"继续", "取消"))
{
return;
}
}
else
{
string list = string.Join(", ", selected);
if (!EditorUtility.DisplayDialog("确认",
$"模式:{modeName}\n平台:{list}\n\n将对 {_previewPaths.Count} 个文件应用。继续?",
"应用", "取消"))
{
return;
}
}

int modified = 0, skipped = 0;
var modifiedPaths = new List<string>();

foreach (string path in _previewPaths)
{
bool changed = ModifyAsmdef(path, data =>
{
if (isInclude)
{
bool c = !ArrayEquals(data.includePlatforms, selected)
|| (data.excludePlatforms != null && data.excludePlatforms.Length > 0);
data.includePlatforms = selected;
data.excludePlatforms = new string[0];
return c;
}
else
{
bool c = !ArrayEquals(data.excludePlatforms, selected)
|| (data.includePlatforms != null && data.includePlatforms.Length > 0);
data.excludePlatforms = selected;
data.includePlatforms = new string[0];
return c;
}
});

if (changed) { modified++; modifiedPaths.Add(path); }
else skipped++;
}

AssetDatabase.Refresh();

string msg = $"处理完成({modeName})。\n\n修改:{modified} 个\n跳过(无变化):{skipped} 个";
if (modifiedPaths.Count > 0)
{
msg += "\n\n修改的文件:\n" + string.Join("\n", modifiedPaths);
Debug.Log($"[BatchAsmdefTool] 批量替换平台完成。\n" + msg);
}
EditorUtility.DisplayDialog("批量替换平台", msg, "确定");
}

private static bool ArrayEquals(string[] a, string[] b)
{
if (a == null && b == null) return true;
if (a == null || b == null) return false;
if (a.Length != b.Length) return false;
var sa = new HashSet<string>(a);
foreach (var x in b) if (!sa.Contains(x)) return false;
return true;
}

// =====================================================================
// 查找 asmdef
// =====================================================================

private static List<string> FindAllAsmdefs(List<string> folders)
{
var result = new HashSet<string>();

foreach (string folder in folders)
{
if (!AssetDatabase.IsValidFolder(folder))
{
Debug.LogWarning($"[BatchAsmdefTool] 文件夹无效:{folder}");
continue;
}

// 注意:t:asmdef 是 Unity 内部注册的类型标识符
string[] guids = AssetDatabase.FindAssets("t:asmdef", new[] { folder });
foreach (string guid in guids)
{
string path = AssetDatabase.GUIDToAssetPath(guid);
// 排除 .asmref
if (path.EndsWith(".asmdef"))
result.Add(path);
}
}

return result.OrderBy(p => p).ToList();
}

// =====================================================================
// asmdef 文件读写(统一入口)
// =====================================================================

/// <summary>
/// 读取 asmdef,调用 modifier 修改数据,如果 modifier 返回 true 则写回文件。
/// 返回 true 表示文件被修改。
/// </summary>
private static bool ModifyAsmdef(string assetPath, System.Func<AssemblyDefinitionData, bool> modifier)
{
string fullPath = Path.Combine(Directory.GetCurrentDirectory(), assetPath);

if (!File.Exists(fullPath))
{
Debug.LogWarning($"[BatchAsmdefTool] 文件不存在:{fullPath}");
return false;
}

string json = File.ReadAllText(fullPath);

AssemblyDefinitionData data;
try
{
data = JsonUtility.FromJson<AssemblyDefinitionData>(json);
}
catch (System.Exception e)
{
Debug.LogWarning($"[BatchAsmdefTool] 解析失败:{assetPath}\n{e.Message}");
return false;
}

if (data == null)
{
Debug.LogWarning($"[BatchAsmdefTool] 解析结果为空:{assetPath}");
return false;
}

bool changed;
try
{
changed = modifier(data);
}
catch (System.Exception e)
{
Debug.LogWarning($"[BatchAsmdefTool] 修改数据失败:{assetPath}\n{e.Message}");
return false;
}

if (!changed)
return false;

string newJson = JsonUtility.ToJson(data, true);
// Unity 原生 asmdef 用 4 空格缩进,JsonUtility 默认 2 空格,做个简单替换
newJson = newJson.Replace(" ", " ");

File.WriteAllText(fullPath, newJson);
return true;
}

private static string[] ParseDefines(string raw)
{
if (string.IsNullOrWhiteSpace(raw))
return new string[0];

return raw
.Split(new[] { '\n', '\r', ',', ';' }, System.StringSplitOptions.RemoveEmptyEntries)
.Select(s => s.Trim())
.Where(s => !string.IsNullOrEmpty(s))
.Distinct()
.ToArray();
}

// =====================================================================
// 数据结构
// =====================================================================

[System.Serializable]
private class AssemblyDefinitionData
{
public string name = "";
public string[] rootNamespace = new string[0];
public string[] references = new string[0];
public string[] includePlatforms = new string[0];
public string[] excludePlatforms = new string[0];
public bool allowUnsafeCode = false;
public bool overrideReferences = false;
public string[] precompiledReferences = new string[0];
public bool autoReferenced = true;
public string[] defineConstraints = new string[0];
public VersionDefine[] versionDefines = new VersionDefine[0];
public bool noEngineReferences = false;
}

[System.Serializable]
private class VersionDefine
{
public string name = "";
public string expression = "";
public string define = "";
}
}
}
}

        我偷了一下懒,我用Deepseek生成了这段代码。个人感觉这效果还算不错。AI真是太好用了。

闲言碎语

        我个人的工程其实并没有使用这个工具,上面的实现是以我项目中的实现为原型改的。之所以这样的做,其实就是因为之前为了赶工,我只做了特定的处理。所以我认为我项目中写的方式不够好。文章中的实现是我闲下来然后进行的修改。本来我还想学着用一下USS。我以为这是一个机会。但是USS真的太难用了,很多东西都要我去做额外的类,然后我就放弃了。

        宏切换工具在我自己的项目里面也是没用到的,我可是手动一个修改过去的。这事情非常的累,而且Unity编译也编译了半天。当时这件事情十分的折磨我。我没想到用代码修改也很麻烦,幸好有AI辅助。