using UnityEngine; using UnityEditor; using System.Collections.Generic; using System.IO; using System.Text; using System; using System.Text.RegularExpressions; using UnityEngine.Rendering; using LitJson; namespace WeChatWASM { /// /// 脚本调用的话可修改 Assets/WX-WASM-SDK/Editor/MiniGameConfig.asset配置,然后调用WXEditorWindow 的 DoExport方法导出小游戏 /// public class WXEditorWindow : EditorWindow { string projectName = ""; string appid = ""; string cdn = ""; string videoUrl = ""; public static string dst = ""; string streamCDN = ""; int bundleHashLength = 32; string bundlePathIdentifier = "StreamingAssets;"; string bundleExcludeExtensions = "json;"; string preloadFiles = ""; // 预下载文件名, 以,分隔文件 // string audioPrefix = ""; bool useAudioApi = false; bool useFriendRelation = false; bool developBuild = false; bool autoProfile = false; bool scriptOnly = false; bool profilingFuncs = false; bool profilingMemory = false; bool deleteStreamingAssets = true; int assetLoadType = 0; // 首包资源加载方式 bool webgl2 = false; int orientation = 0; static string SDKFilePath = ""; public static WXEditorScriptObject config; public static string webglDir = "webgl"; //导出的webgl目录 public static string miniGameDir = "minigame"; // 生成小游戏的目录 public static string audioDir = "Assets"; //音频资源目录 public string codeMd5 = ""; public string dataMd5 = ""; public string dataFileSize = ""; public Texture tex; public string defaultImgSrc = "Assets/WX-WASM-SDK/wechat-default/images/background.jpg"; public string bgImageSrc = ""; public int memorySize = 0; public bool hideAfterCallMain = true; public string dataFileSubPrefix = ""; public int maxStorage = 200; public int defaultReleaseSize = 31457280; public int texturesHashLength = 8; public string texturesPath = "Assets/Textures"; public bool needCacheTextures = false; public int loadingBarWidth = 240; public bool needCheckUpdate = false; [MenuItem("微信小游戏 / 转换小游戏", false, 1)] public static void Open() { #if !(UNITY_2018_1_OR_NEWER) UnityEngine.Debug.LogError("目前仅支持 Unity2018及以上的版本!"); #endif var win = GetWindow(typeof(WXEditorWindow), false, "转换微信小游戏", true);//创建窗口 win.minSize = new Vector2(650, 800); win.maxSize = new Vector2(1600, 950); win.Show(); // 打开面板时自动检查更新 PluginUpdateManager.CheckUpdte(); Init(); } public static void Init() { PlayerSettings.WebGL.threadsSupport = false; PlayerSettings.runInBackground = false; PlayerSettings.WebGL.compressionFormat = WebGLCompressionFormat.Disabled; #if UNITY_2020_1_OR_NEWER PlayerSettings.WebGL.template = "PROJECT:WXTemplate2020"; #else PlayerSettings.WebGL.template = "PROJECT:WXTemplate"; #endif PlayerSettings.WebGL.linkerTarget = WebGLLinkerTarget.Wasm; PlayerSettings.WebGL.dataCaching = false; #if UNITY_2021_2_OR_NEWER PlayerSettings.WebGL.debugSymbolMode = WebGLDebugSymbolMode.Embedded; #else PlayerSettings.WebGL.debugSymbols = true; #endif EditorSettings.spritePackerMode = SpritePackerMode.AlwaysOnAtlas; } public void OnEnable() { Init(); LoadData(); UpdateGraphicAPI(); } public void LoadData() { SDKFilePath = Path.Combine(Application.dataPath, "WX-WASM-SDK", "wechat-default", "unity-sdk", "index.js"); config = UnityUtil.GetEditorConf(); projectName = config.ProjectConf.projectName; appid = config.ProjectConf.Appid; cdn = config.ProjectConf.CDN; assetLoadType = config.ProjectConf.assetLoadType; videoUrl = config.ProjectConf.VideoUrl; orientation = (int)config.ProjectConf.Orientation; dst = config.ProjectConf.DST; // streamCDN = config.ProjectConf.StreamCDN; bundleHashLength = config.ProjectConf.bundleHashLength; bundlePathIdentifier = config.ProjectConf.bundlePathIdentifier; bundleExcludeExtensions = config.ProjectConf.bundleExcludeExtensions; preloadFiles = config.ProjectConf.preloadFiles; developBuild = config.CompileOptions.DevelopBuild; autoProfile = config.CompileOptions.AutoProfile; scriptOnly = config.CompileOptions.ScriptOnly; profilingFuncs = config.CompileOptions.profilingFuncs; profilingMemory = config.CompileOptions.ProfilingMemory; deleteStreamingAssets = config.CompileOptions.DeleteStreamingAssets; webgl2 = config.CompileOptions.Webgl2; useAudioApi = config.SDKOptions.UseAudioApi; // audioPrefix = config.ProjectConf.AssetsUrl; useFriendRelation = config.SDKOptions.UseFriendRelation; bgImageSrc = config.ProjectConf.bgImageSrc; tex = AssetDatabase.LoadAssetAtPath(bgImageSrc); memorySize = config.ProjectConf.MemorySize; hideAfterCallMain = config.ProjectConf.HideAfterCallMain; // 不常用配置,先只通过MiniGameConfig.assets修改 dataFileSubPrefix = config.ProjectConf.dataFileSubPrefix; maxStorage = config.ProjectConf.maxStorage; defaultReleaseSize = config.ProjectConf.defaultReleaseSize; texturesHashLength = config.ProjectConf.texturesHashLength; texturesPath = config.ProjectConf.texturesPath; needCacheTextures = config.ProjectConf.needCacheTextures; loadingBarWidth = config.ProjectConf.loadingBarWidth; needCheckUpdate = config.ProjectConf.needCheckUpdate; } private void OnFocus() { LoadData(); } private void OnDisable() { EditorUtility.SetDirty(config); } private void OnLostFocus() { config.ProjectConf.projectName = projectName; config.ProjectConf.Appid = appid; config.ProjectConf.CDN = cdn; config.ProjectConf.assetLoadType = assetLoadType; config.ProjectConf.VideoUrl = videoUrl; config.ProjectConf.Orientation = (WXScreenOritation)orientation; config.ProjectConf.DST = dst; // config.ProjectConf.StreamCDN = streamCDN; config.ProjectConf.bundleHashLength = bundleHashLength; config.ProjectConf.bundlePathIdentifier = bundlePathIdentifier; config.ProjectConf.bundleExcludeExtensions = bundleExcludeExtensions; config.ProjectConf.preloadFiles = preloadFiles; config.CompileOptions.DevelopBuild = developBuild; config.CompileOptions.AutoProfile = autoProfile; config.CompileOptions.ScriptOnly = scriptOnly; config.CompileOptions.profilingFuncs = profilingFuncs; config.CompileOptions.ProfilingMemory = profilingMemory; config.CompileOptions.DeleteStreamingAssets = deleteStreamingAssets; config.CompileOptions.Webgl2 = webgl2; config.SDKOptions.UseAudioApi = useAudioApi; // config.ProjectConf.AssetsUrl = audioPrefix; config.SDKOptions.UseFriendRelation = useFriendRelation; config.ProjectConf.bgImageSrc = bgImageSrc; config.ProjectConf.MemorySize = memorySize; config.ProjectConf.HideAfterCallMain = hideAfterCallMain; config.ProjectConf.dataFileSubPrefix = dataFileSubPrefix; config.ProjectConf.maxStorage = maxStorage; config.ProjectConf.defaultReleaseSize = defaultReleaseSize; config.ProjectConf.texturesHashLength = texturesHashLength; config.ProjectConf.texturesPath = texturesPath; config.ProjectConf.needCacheTextures = needCacheTextures; config.ProjectConf.loadingBarWidth = loadingBarWidth; config.ProjectConf.needCheckUpdate = needCheckUpdate; } static string[] GetScenePaths() { List scenes = new List(); for (int i = 0; i < EditorBuildSettings.scenes.Length; i++) { var scene = EditorBuildSettings.scenes[i]; UnityEngine.Debug.LogFormat("[Builder] Scenes [{0}]: {1}, [{2}]", i, scene.path, scene.enabled ? "x" : " "); if (scene.enabled) { scenes.Add(scene.path); } } return scenes.ToArray(); } private int Build() { PlayerSettings.WebGL.emscriptenArgs = ""; PlayerSettings.runInBackground = false; if (memorySize != 0) { if (memorySize >= 1024) { UnityEngine.Debug.LogErrorFormat($"memorySize必须小于1024,请查看GIT文档优化Unity WebGL的内存"); return -1; } PlayerSettings.WebGL.emscriptenArgs += $" -s TOTAL_MEMORY={memorySize}MB"; } if (profilingMemory) { PlayerSettings.WebGL.emscriptenArgs += " --memoryprofiler "; } if (profilingFuncs) { #if !(UNITY_2021_2_OR_NEWER) PlayerSettings.WebGL.emscriptenArgs += " --profiling-funcs"; #else PlayerSettings.WebGL.debugSymbolMode = WebGLDebugSymbolMode.Embedded; #endif } UnityEngine.Debug.Log("[Builder] Starting to build WebGL project ... "); UnityEngine.Debug.Log("PlayerSettings.WebGL.emscriptenArgs : "+ PlayerSettings.WebGL.emscriptenArgs); // PlayerSettings.WebGL.memorySize = memorySize; BuildOptions option = BuildOptions.None; if (developBuild) { option |= BuildOptions.Development; } if (autoProfile) { option |= BuildOptions.ConnectWithProfiler; } if (scriptOnly) { option |= BuildOptions.BuildScriptsOnly; } if (EditorUserBuildSettings.activeBuildTarget != BuildTarget.WebGL) { UnityEngine.Debug.LogFormat("[Builder] Current target is: {0}, switching to: {1}", EditorUserBuildSettings.activeBuildTarget, BuildTarget.WebGL); if (!EditorUserBuildSettings.SwitchActiveBuildTarget(BuildTargetGroup.WebGL, BuildTarget.WebGL)) { UnityEngine.Debug.LogFormat("[Builder] Switching to {0}/{1} failed!", BuildTargetGroup.WebGL, BuildTarget.WebGL); return -1; } } var projDir = Path.Combine(dst, webglDir); var result = BuildPipeline.BuildPlayer(GetScenePaths(), projDir, BuildTarget.WebGL, option); if (result.summary.result != UnityEditor.Build.Reporting.BuildResult.Succeeded) { UnityEngine.Debug.LogFormat($"[Builder] BuildPlayer failed. emscriptenArgs:%s", PlayerSettings.WebGL.emscriptenArgs); return -1; } UnityEngine.Debug.LogFormat("[Builder] Done: " + projDir); return 0; } private static bool CopyDirectory(string SourcePath, string DestinationPath, bool overwriteexisting) { bool ret = false; var separator = Path.DirectorySeparatorChar; var ignoreFiles = new List() { "unityNamespace.js" }; // eventEmitter - 改名为event-emitter // loading和libs 是可交互视频用到的文件,先下掉可交互方案 var ignoreDirs = new List() { "eventEmitter", "loading", "libs" }; try { if (Directory.Exists(SourcePath)) { if (Directory.Exists(DestinationPath) == false) { Directory.CreateDirectory(DestinationPath); } else { // 已经存在,删掉目录下无用的文件 foreach(string filename in ignoreFiles) { var filepath = Path.Combine(DestinationPath, filename); if (File.Exists(filepath)) { File.Delete(filepath); } } foreach(string dir in ignoreDirs) { var dirpath = Path.Combine(DestinationPath, dir); if (Directory.Exists(dirpath)) { Directory.Delete(dirpath); } } } foreach (string fls in Directory.GetFiles(SourcePath)) { FileInfo flinfo = new FileInfo(fls); if (flinfo.Extension == ".meta" || ignoreFiles.Contains(flinfo.Name)) { continue; } flinfo.CopyTo(Path.Combine(DestinationPath, flinfo.Name), overwriteexisting); } foreach (string drs in Directory.GetDirectories(SourcePath)) { DirectoryInfo drinfo = new DirectoryInfo(drs); if (ignoreDirs.Contains(drinfo.Name)) { continue; } if (CopyDirectory(drs, Path.Combine(DestinationPath, drinfo.Name), overwriteexisting) == false) ret = false; } } ret = true; } catch (Exception ex) { ret = false; UnityEngine.Debug.LogError(ex); } return ret; } private static bool CopyMusicDirectory(string SourcePath, string DestinationPath, bool overwriteexisting) { bool ret = false; try { if (Directory.Exists(SourcePath)) { foreach (string fls in Directory.GetFiles(SourcePath)) { FileInfo flinfo = new FileInfo(fls); string[] suffix = { ".wav",".mp3", "m4a", "aac", "mp4" }; if (Array.IndexOf(suffix, flinfo.Extension.ToLower())>-1) { UnityUtil.CreateDir(DestinationPath); flinfo.CopyTo(Path.Combine(DestinationPath, flinfo.Name), overwriteexisting); } } foreach (string drs in Directory.GetDirectories(SourcePath)) { DirectoryInfo drinfo = new DirectoryInfo(drs); if (CopyMusicDirectory(drs, Path.Combine(DestinationPath, drinfo.Name), overwriteexisting) == false) ret = false; } } ret = true; } catch (Exception ex) { ret = false; UnityEngine.Debug.LogError(ex); } return ret; } private void ConvertCode() { UnityEngine.Debug.LogFormat("[Converter] Starting to adapt framewor. Dst: " + dst); UnityUtil.DelectDir(Path.Combine(dst, miniGameDir)); #if UNITY_2020_1_OR_NEWER string text = File.ReadAllText(Path.Combine(dst, webglDir, "Build", "webgl.framework.js"), Encoding.UTF8); #else string text = File.ReadAllText(Path.Combine(dst, webglDir, "Build", "webgl.wasm.framework.unityweb"), Encoding.UTF8); #endif int i; for (i = 0; i < ReplaceRules.rules.Length; i++) { var rule = ReplaceRules.rules[i]; text = Regex.Replace(text, rule.old, rule.newStr); } if (PlayerSettings.WebGL.exceptionSupport == WebGLExceptionSupport.None) { Rule[] rules = { new Rule() { old = "console.log\\(\"Exception at", newStr= "if(Module.IsWxGame);console.log(\"Exception at" }, new Rule() { old = "throw ptr", newStr = "if(Module.IsWxGame)window.WXWASMSDK.WXUncaughtException(true);else throw ptr" }, }; foreach (var rule in rules) { text = Regex.Replace(text, rule.old, rule.newStr); } } if (text.Contains("UnityModule")) { text += ";GameGlobal.unityNamespace.UnityModule = UnityModule;"; } else { if (text.StartsWith("(") && text.EndsWith(")")) { text = text.Substring(1, text.Length - 2); } text = "GameGlobal.unityNamespace.UnityModule = " + text; } if (!Directory.Exists(Path.Combine(dst, miniGameDir))) { Directory.CreateDirectory(Path.Combine(dst, miniGameDir)); } File.WriteAllText(Path.Combine(dst, miniGameDir, "webgl.wasm.framework.unityweb.js"), text, new UTF8Encoding(false)); UnityEngine.Debug.LogFormat("[Converter] adapt framework done! "); } /// /// 删掉导出目录webgl目录下旧资源包 /// private void RemoveOldAssetPackage(string dstDir) { try { if (Directory.Exists(dstDir)) { foreach (string path in Directory.GetFiles(dstDir)) { FileInfo fileInfo = new FileInfo(path); if (fileInfo.Name.Contains("webgl.data.unityweb.bin.txt")) { File.Delete(fileInfo.FullName); } } } } catch (Exception ex) { UnityEngine.Debug.LogError(ex); } } /// /// 等brotli之后,统计下资源包加brotli压缩后代码包是否超过了20M(小游戏代码分包总大小限制) /// private void checkNeedCopyDataPackage(bool brotliError) { // 如果brotli失败,使用CDN加载 if (brotliError) { // brotli失败后,因为无法知道wasmcode大小,则得不到最终小游戏总包体大小。不能使用小游戏分包加载资源,还原成cdn的方式。 if (assetLoadType == 1) { UnityEngine.Debug.LogWarning("brotli失败,无法检测文件大小,请上传资源文件到CDN"); assetLoadType = 0; } ShowNotification(new GUIContent("Brotli压缩失败,请到转出目录手动压缩!!!")); } if ((assetLoadType == 1)) { #if UNITY_2020_1_OR_NEWER var dataPath = Path.Combine(dst, webglDir, "Build", "webgl.data"); #else var dataPath = Path.Combine(dst, webglDir, "Build", "webgl.data.unityweb"); #endif var brcodePath = Path.Combine(dst, miniGameDir, "wasmcode", codeMd5 + ".webgl.wasm.code.unityweb.wasm.br"); var brcodeInfo = new FileInfo(brcodePath); var brcodeSize = brcodeInfo.Length; if (brcodeSize + int.Parse(dataFileSize) > 20971520) { ShowNotification(new GUIContent("资源文件过大,不适宜用代码分包加载")); throw new Exception("资源文件过大,不适宜用代码分包加载"); } else { File.Copy(dataPath, Path.Combine(dst, miniGameDir, "data-package", dataMd5 + ".webgl.data.unityweb.bin.txt"), true); } } var loadDataFromCdn = assetLoadType == 0; Rule[] rules = { new Rule() { old="$DEPLOY_URL", newStr= cdn }, new Rule() { old="$LOAD_DATA_FROM_SUBPACKAGE", newStr = loadDataFromCdn ? "false" : "true" } }; string[] files = { "game.js", "game.json", "project.config.json" }; ReplaceFileContent(files, rules); } public int GenerateBinFile(bool isFromConvert = false) { UnityEngine.Debug.LogFormat("[Converter] Starting to genarate md5 and copy files"); #if UNITY_2020_1_OR_NEWER var codePath = Path.Combine(dst, webglDir, "Build", "webgl.wasm"); #else var codePath = Path.Combine(dst, webglDir, "Build", "webgl.wasm.code.unityweb"); #endif codeMd5 = UnityUtil.BuildFileMd5(codePath); #if UNITY_2020_1_OR_NEWER var dataPath = Path.Combine(dst, webglDir, "Build", "webgl.data"); #else var dataPath = Path.Combine(dst, webglDir, "Build", "webgl.data.unityweb"); #endif dataMd5 = UnityUtil.BuildFileMd5(dataPath); #if UNITY_2020_1_OR_NEWER var symbolPath = Path.Combine(dst, webglDir, "Build", "webgl.symbols.json"); #else var symbolPath = Path.Combine(dst, webglDir, "Build", "webgl.wasm.symbols.unityweb"); #endif RemoveOldAssetPackage(Path.Combine(dst, webglDir)); RemoveOldAssetPackage(Path.Combine(dst, webglDir+"-min")); File.Copy(dataPath, Path.Combine(dst, webglDir, dataMd5 + ".webgl.data.unityweb.bin.txt"), true); CopyDirectory(Path.Combine(Application.dataPath, "WX-WASM-SDK", "wechat-default"), Path.Combine(dst, miniGameDir), true); // FIX: 2021.2版本生成symbol有bug,导出时生成symbol报错,有symbol才copy // 代码分包需要symbol文件以进行增量更新 if (File.Exists(symbolPath)) { File.Copy(symbolPath, Path.Combine(dst, miniGameDir, "webgl.wasm.symbols.unityweb"), true); } var info = new FileInfo(dataPath); dataFileSize = info.Length.ToString(); UnityEngine.Debug.LogFormat("[Converter] that to genarate md5 and copy files ended"); ModifyWeChatConfigs(isFromConvert); ModifySDKFile(); ClearFriendRelationCode(); if (useAudioApi) { CopyMusicDirectory(Application.dataPath, Path.Combine(dst, webglDir ,audioDir), true); } // 如果没有StreamingAssets目录,默认生成 if (!Directory.Exists(Path.Combine(dst, webglDir, "StreamingAssets"))) { Directory.CreateDirectory(Path.Combine(dst, webglDir, "StreamingAssets")); } return Brotlib(codePath); } private void ModifySDKFile() { var config = UnityUtil.GetEditorConf(); string content = File.ReadAllText(SDKFilePath, Encoding.UTF8); content = content.Replace("$unityVersion$", Application.unityVersion); File.WriteAllText(Path.Combine(dst, miniGameDir, "unity-sdk", "index.js"), content, Encoding.UTF8); content = File.ReadAllText(Path.Combine(Application.dataPath, "WX-WASM-SDK", "wechat-default", "unity-sdk", "storage.js"), Encoding.UTF8); var PreLoadKeys = config.PlayerPrefsKeys.Count > 0 ? JsonMapper.ToJson(config.PlayerPrefsKeys) : "[]"; content = content.Replace("\"$PreLoadKeys\"", PreLoadKeys); File.WriteAllText(Path.Combine(dst, miniGameDir, "unity-sdk", "storage.js"), content, Encoding.UTF8); //修改纹理dxt content = File.ReadAllText(Path.Combine(Application.dataPath, "WX-WASM-SDK", "wechat-default", "unity-sdk", "texture.js"), Encoding.UTF8); content = content.Replace("\"$UseDXT5$\"", config.CompressTexture.useDXT5 ? "true" : "false"); File.WriteAllText(Path.Combine(dst, miniGameDir, "unity-sdk", "texture.js"), content, Encoding.UTF8); } class PreloadFile { public PreloadFile(string fn, string rp) { fileName = fn; relativePath = rp; } public string fileName; public string relativePath; }; /// /// 从webgl目录模糊搜索preloadfiles中的文件,作为预下载的列表 /// private string GetPreloadList(string strPreloadfiles) { if (strPreloadfiles == "") { return ""; } string preloadList = ""; var streamingAssetsPath = Path.Combine(dst, webglDir + "/StreamingAssets"); var fileNames = strPreloadfiles.Split(new char[] { ';' }); List preloadFiles = new List(); foreach(var fileName in fileNames) { if (fileName.Trim() == "") continue; preloadFiles.Add(new PreloadFile(fileName, "")); } if (Directory.Exists(streamingAssetsPath)) { foreach (string path in Directory.GetFiles(streamingAssetsPath, "*", SearchOption.AllDirectories)) { FileInfo fileInfo = new FileInfo(path); foreach (var preloadFile in preloadFiles) { if (fileInfo.Name.Contains(preloadFile.fileName)) { // 相对于StreamingAssets的路径 var relativePath = path.Substring(streamingAssetsPath.Length + 1).Replace('\\', '/'); preloadFile.relativePath = relativePath; break; } } } } else { UnityEngine.Debug.LogError("没有找到StreamingAssets目录, 无法生成预下载列表"); } foreach (var preloadFile in preloadFiles) { if (preloadFile.relativePath == "") { UnityEngine.Debug.LogError($"并非所有预下载的文件都被找到,剩余:{preloadFile.fileName}"); continue; } preloadList += ("\"" + preloadFile.relativePath + "\", \r"); } return preloadList; } public string HandleLoadingImage() { var info = AssetDatabase.LoadAssetAtPath(bgImageSrc); var oldFilename = Path.GetFileName(defaultImgSrc); var newFilename = Path.GetFileName(bgImageSrc); if (bgImageSrc != defaultImgSrc) { // 图片宽高不能超过2048 if (info.width > 2048 || info.height > 2048) { throw new Exception("封面图宽高不可超过2048"); } File.Delete(Path.Combine(dst, miniGameDir, "images", oldFilename)); File.Copy(bgImageSrc, Path.Combine(dst, miniGameDir, "images", newFilename), true); return "images/" + Path.GetFileName(bgImageSrc); } else { return "images/" + Path.GetFileName(defaultImgSrc); } } public void ModifyWeChatConfigs(bool isFromConvert = false) { UnityEngine.Debug.LogFormat("[Converter] Starting to modify configs"); var config = UnityUtil.GetEditorConf(); var PRELOAD_LIST = GetPreloadList(preloadFiles); var imgSrc = HandleLoadingImage(); var bundlePathIdentifierStr = GetArrayString(bundlePathIdentifier); var excludeFileExtensionsStr = GetArrayString(bundleExcludeExtensions); var screenOrientation = new List(){"portrait", "landscape", "landscapeLeft", "landscapeRight"}[orientation]; Rule[] replaceArrayList = { new Rule() { old="$GAME_NAME", newStr="webgl" }, new Rule() { old="$PROJECT_NAME", newStr=projectName == "" ? "webgl" : projectName, }, new Rule() { old="$APP_ID", newStr=appid }, new Rule() { old="$ORIENTATION", newStr=screenOrientation }, new Rule() { old="$LOADING_VIDEO_URL", newStr=videoUrl }, new Rule() { old="$CODE_MD5", newStr=codeMd5 }, new Rule() { old="$DATA_MD5", newStr=dataMd5 }, // new Rule() // { // old="$DATA_FILE_SIZE", // newStr=dataFileSize // }, new Rule() { old="$STREAM_CDN", newStr=streamCDN }, new Rule() { old="$AUDIO_PREFIX", newStr=cdn + "/Assets" }, new Rule() { old="\"$PRELOAD_LIST\"", newStr=PRELOAD_LIST }, new Rule() { old="$BACKGROUND_IMAGE", newStr=imgSrc }, new Rule() { old="$HIDE_AFTER_CALLMAIN", newStr = hideAfterCallMain ? "true" : "false" }, new Rule() { old="$BUNDLE_HASH_LENGTH", newStr=bundleHashLength.ToString() }, new Rule() { old="$BUNDLE_PATH_IDENTIFIER", newStr=bundlePathIdentifierStr }, new Rule() { old="$EXCLUDE_FILE_EXTENSIONS", newStr=excludeFileExtensionsStr }, new Rule() { old="$WEBGL_VERSION", newStr=webgl2? "2" : "1" }, new Rule() { old="$UNITY_VERSION", newStr=Application.unityVersion }, new Rule() { old="$PLUGIN_VERSION", newStr=WXPluginVersion.pluginVersion }, new Rule() { old="$DATA_FILE_SUB_PREFIX", newStr=dataFileSubPrefix }, new Rule() { old="$MAX_STORAGE_SIZE", newStr=maxStorage.ToString() }, new Rule() { old="$DEFAULT_RELEASE_SIZE", newStr=defaultReleaseSize.ToString() }, new Rule() { old="$TEXTURE_HASH_LENGTH", newStr=texturesHashLength.ToString() }, new Rule() { old="$TEXTURES_PATH", newStr=texturesPath }, new Rule() { old="$NEED_CACHE_TEXTURES", newStr=needCacheTextures ? "true" : "false" }, new Rule() { old="$LOADING_BAR_WIDTH", newStr=loadingBarWidth.ToString() }, new Rule() { old="$NEED_CHECK_UPDATE", newStr=needCheckUpdate ? "true" : "false" } }; List replaceList = new List(replaceArrayList); List files = new List { "game.js", "game.json", "project.config.json", "unity-namespace.js" }; ReplaceFileContent(files.ToArray(), replaceList.ToArray()); UnityEngine.Debug.LogFormat("[Converter] that to modify configs ended"); } /// /// 对文件做内容替换 /// /// /// public void ReplaceFileContent(string[] files, Rule[] replaceList) { if (files.Length != 0 && replaceList.Length != 0) { for (int i = 0; i < files.Length; i++) { var filePath = Path.Combine(dst, miniGameDir, files[i]); string text = File.ReadAllText(filePath, Encoding.UTF8); for (int j = 0; j < replaceList.Length; j++) { var rule = replaceList[j]; text = text.Replace(rule.old, rule.newStr); } File.WriteAllText(filePath, text, new UTF8Encoding(false)); } } } /// /// 按;分隔字符串,将分隔后每一项作为字符串用,连接 /// eg: input "i1;i2;i3" => output: `"i1", "i2", "i3"` /// /// public string GetArrayString(string inp) { var result = ""; var iterms = new List(inp.Split(new char[] {';'})); iterms.ForEach((iterm) => { if (!string.IsNullOrEmpty(iterm.Trim())) { result += ("\"" + iterm.Trim() + "\", "); } }); if (!string.IsNullOrEmpty(result)) { result = result.Substring(0, result.Length - 2); } return result; } private int Brotlib(string filePath) { UnityEngine.Debug.LogFormat("[Converter] Starting to generate Brotlib file"); var exePath = ""; var path = ""; #if UNITY_EDITOR_OSX exePath = Path.Combine(Application.dataPath, "WX-WASM-SDK/Editor/Brotli/macos/brotli"); #else exePath = Path.Combine(Application.dataPath, "WX-WASM-SDK/Editor/Brotli/win_x86_64/brotli.exe"); #endif var dstPath = Path.Combine(dst, miniGameDir, "wasmcode", codeMd5 + ".webgl.wasm.code.unityweb.wasm.br"); WeChatWASM.UnityUtil.RunCmd(exePath, string.Format($" --force --quality 11" + $" --input {filePath}" + $" --output {dstPath}"), path); return 0; } /// /// 如果没有使用好友关系链的话,自动删掉无用代码 /// private void ClearFriendRelationCode() { if (!useFriendRelation) { var filePath = Path.Combine(dst, miniGameDir, "game.json"); string content = File.ReadAllText(filePath, Encoding.UTF8); content = content.Replace("\"openDataContext\": \"open-data\",", ""); File.WriteAllText(filePath, content); string openDataDir = Path.Combine(dst, miniGameDir, "open-data"); UnityUtil.DelectDir(openDataDir); Directory.Delete(openDataDir, true); } } public static void DrawProObjectField( GUIContent label, SerializedProperty value, Type objType, GUIStyle style, bool allowSceneObjects, Texture objIcon = null) where T : UnityEngine.Object { T tObj = value.objectReferenceValue as T; if (objIcon == null) { objIcon = EditorGUIUtility.FindTexture("PrefabNormal Icon"); } style.imagePosition = ImagePosition.ImageLeft; int pickerID = 455454425; if (tObj != null) { EditorGUILayout.LabelField(label, new GUIContent(tObj.name, objIcon), style); } if (GUILayout.Button("Select")) { EditorGUIUtility.ShowObjectPicker( tObj, allowSceneObjects, "", pickerID); } if (Event.current.commandName == "ObjectSelectorUpdated") { if (EditorGUIUtility.GetObjectPickerControlID() == pickerID) { tObj = EditorGUIUtility.GetObjectPickerObject() as T; value.objectReferenceValue = tObj; } } } private void OnGUI() { var labelStyle = new GUIStyle(EditorStyles.boldLabel); labelStyle.fontSize = 14; labelStyle.margin.left = 20; labelStyle.margin.top = 10; labelStyle.margin.bottom = 10; GUILayout.Label("基本设置", labelStyle); var inputStyle = new GUIStyle(EditorStyles.textField); inputStyle.fontSize = 14; inputStyle.margin.left = 20; inputStyle.margin.bottom = 10; inputStyle.margin.right = 20; var intPopupStyle = new GUIStyle(EditorStyles.popup); intPopupStyle.fontSize = 14; intPopupStyle.margin.left = 20; intPopupStyle.margin.bottom = 15; intPopupStyle.margin.right = 20; appid = EditorGUILayout.TextField("游戏appid", appid, inputStyle); cdn = EditorGUILayout.TextField("游戏资源CDN", cdn, inputStyle); projectName = EditorGUILayout.TextField("小游戏项目名", projectName, inputStyle); orientation = EditorGUILayout.IntPopup("游戏方向", orientation, new[] { "Portrait", "Landscape", "LandscapeLeft", "LandscapeRight" }, new[] { 0, 1, 2, 3 }, intPopupStyle); var totalMemoryFieldDesc = new GUIContent("最大内存(MB)", "预留的初始内存值,需评估游戏最大内存峰值进行设置,消除内存自动增长带来的峰值尖刺。请查看GIT文档<优化Unity WebGL的内存>"); memorySize = EditorGUILayout.IntField(totalMemoryFieldDesc, memorySize, inputStyle); GUILayout.Label("导出路径", labelStyle); var choosePathButtonClicked = false; var openTargetButtonClicked = false; var resetButtonClicked = false; if (dst == "") { GUIStyle pathButtonStyle = new GUIStyle(GUI.skin.button); pathButtonStyle.fontSize = 12; pathButtonStyle.margin.left = 20; choosePathButtonClicked = GUILayout.Button("选择导出路径", pathButtonStyle, GUILayout.Height(30), GUILayout.Width(200)); } else { int pathButtonHeight = 28; GUIStyle pathLabelStyle = new GUIStyle(GUI.skin.textField); pathLabelStyle.fontSize = 12; pathLabelStyle.alignment = TextAnchor.MiddleLeft; pathLabelStyle.margin.top = 6; pathLabelStyle.margin.bottom = 6; pathLabelStyle.margin.left = 20; GUILayout.BeginHorizontal(); // 路径框 GUILayout.Label(dst, pathLabelStyle, GUILayout.Height(pathButtonHeight - 6), GUILayout.ExpandWidth(true), GUILayout.MaxWidth(EditorGUIUtility.currentViewWidth - 126)); openTargetButtonClicked = GUILayout.Button("打开", GUILayout.Height(pathButtonHeight), GUILayout.Width(40)); resetButtonClicked = GUILayout.Button("重选", GUILayout.Height(pathButtonHeight), GUILayout.Width(40)); GUILayout.EndHorizontal(); } EditorGUILayout.Space(); GUILayout.Label("启动Loader设置", labelStyle); EditorGUILayout.BeginHorizontal(); GUILayout.Space(20); tex = (Texture)EditorGUILayout.ObjectField("启动背景图/视频封面", tex, typeof(Texture2D), false); var currentBgSrc = AssetDatabase.GetAssetPath(tex); TextureImporter texInfo = (TextureImporter)AssetImporter.GetAtPath(currentBgSrc); if (!string.IsNullOrEmpty(currentBgSrc) && currentBgSrc != bgImageSrc) { bgImageSrc = currentBgSrc; var config = UnityUtil.GetEditorConf(); config.ProjectConf.bgImageSrc = bgImageSrc; EditorUtility.SetDirty(config); AssetDatabase.SaveAssets(); } GUILayout.Space(20); EditorGUILayout.EndHorizontal(); videoUrl = EditorGUILayout.TextField("加载阶段视频url", videoUrl, inputStyle); var optionsList = new List(); optionsList.Add(new GUIContent("CDN")); optionsList.Add(new GUIContent("小游戏分包")); GUIContent assetLoadTypeLabel = new GUIContent("首包资源加载方式", "选择'CDN'通过传统CDN加载资源。选择'小游戏分包'通过小游戏代码分包加载资源。小游戏分包有总大小20M限制,若资源加代码总大小超过20M,会自动切换为传统CDN加载"); assetLoadType = EditorGUILayout.IntPopup(assetLoadTypeLabel, assetLoadType, optionsList.ToArray(), new[] { 0, 1 }, intPopupStyle); // audioPrefix = EditorGUILayout.TextField("Assets目录对应CDN地址", audioPrefix, inputStyle); // memorySize = EditorGUILayout.IntField("游戏内存大小(MB)", memorySize, inputStyle); // EditorGUILayout.Space(); // GUILayout.Label("AssetBundle缓存配置", labelStyle); // streamCDN = EditorGUILayout.TextField("Bundle包CDN地址", streamCDN, inputStyle); // var bundlePathFieldDesc = new GUIContent("路径标识符(;分隔)", "下载路径中包含标识符可判定为下载Bundle,可自动缓存。默认值StreamingAssets"); // bundlePathIdentifier = EditorGUILayout.TextField(bundlePathFieldDesc, bundlePathIdentifier, inputStyle); var bundleExcludeExtensionsFieldDesc = new GUIContent("不自动缓存文件类型(;分隔)", "当请求url包含资源'cdn+StreamingAssets'时会自动缓存,但StreamingAssets目录下不是所有文件都需缓存,此选项配置不需要自动缓存的文件拓展名。默认值json"); bundleExcludeExtensions = EditorGUILayout.TextField(bundleExcludeExtensionsFieldDesc, bundleExcludeExtensions, inputStyle); var bundleHashLengthFieldDesc = new GUIContent("Bundle名中Hash长度", "自定义Bundle文件名中hash部分长度,默认值32,用于缓存控制。"); bundleHashLength = EditorGUILayout.IntField(bundleHashLengthFieldDesc, bundleHashLength, inputStyle); EditorGUILayout.Space(); GUILayout.Label("预下载选项", labelStyle); GUILayout.BeginHorizontal(); preloadFiles = EditorGUILayout.TextField("文件列表(;间隔,模糊匹配)", preloadFiles, inputStyle); GUILayout.EndHorizontal(); EditorGUILayout.Space(); GUIStyle toggleStyle = new GUIStyle(GUI.skin.toggle); toggleStyle.margin.left = 20; toggleStyle.margin.right = 20; GUILayout.Label("SDK功能选项", labelStyle); GUILayout.BeginHorizontal(); useFriendRelation = GUILayout.Toggle(useFriendRelation, "使用好友关系链", toggleStyle); useAudioApi = GUILayout.Toggle(useAudioApi, "使用微信音频API", toggleStyle); GUILayout.EndHorizontal(); EditorGUILayout.Space(); EditorGUILayout.Space(); EditorGUILayout.Space(); GUILayout.Label("调试编译选项", labelStyle); GUILayout.BeginHorizontal(); developBuild = GUILayout.Toggle(developBuild, "Development Build",toggleStyle); autoProfile = GUILayout.Toggle(autoProfile, "Autoconnect Profiler", toggleStyle); scriptOnly = GUILayout.Toggle(scriptOnly, "Scripts Only Build", toggleStyle); GUILayout.EndHorizontal(); GUILayout.BeginHorizontal(); profilingFuncs = GUILayout.Toggle(profilingFuncs, "Profiling Funcs ", toggleStyle); profilingMemory = GUILayout.Toggle(profilingMemory, "Profiling Memory ", toggleStyle); var oldwebgl2 = webgl2; webgl2 = GUILayout.Toggle(webgl2, "WebGL2.0(beta)", toggleStyle); if (oldwebgl2 != webgl2) UpdateGraphicAPI(); GUILayout.EndHorizontal(); EditorGUILayout.Space(); deleteStreamingAssets = GUILayout.Toggle(deleteStreamingAssets, "ClearStreamingAssets", toggleStyle); GUIStyle exportButtonStyle = new GUIStyle(GUI.skin.button); exportButtonStyle.fontSize = 14; exportButtonStyle.margin.left = 20; exportButtonStyle.margin.top = 40; EditorGUILayout.BeginHorizontal(); var isExportBtnPressed = GUILayout.Button("导出WEBGL并转换为小游戏(常用)", exportButtonStyle, GUILayout.Height(40), GUILayout.Width(EditorGUIUtility.currentViewWidth - 270)); var isConvertBtnPressed = GUILayout.Button("将WEBGL转为小游戏(不常用)", exportButtonStyle, GUILayout.Height(40), GUILayout.Width(210)); EditorGUILayout.EndHorizontal(); if (isExportBtnPressed) { DoExport(true); } if (isConvertBtnPressed) { DoExport(false); } if (choosePathButtonClicked) { // 弹出选目录窗口 var dstPath = EditorUtility.SaveFolderPanel("选择你的游戏导出目录", "", ""); if (dstPath != "") { dst = dstPath; OnLostFocus(); } } if (openTargetButtonClicked) { UnityUtil.ShowInExplorer(dst); } if (resetButtonClicked) { dst = ""; } EditorGUILayout.Space(); } private void UpdateGraphicAPI() { PlayerSettings.SetUseDefaultGraphicsAPIs(BuildTarget.WebGL, false); GraphicsDeviceType[] targets = new GraphicsDeviceType[] { }; if (webgl2) { PlayerSettings.SetGraphicsAPIs(BuildTarget.WebGL, new GraphicsDeviceType[] { GraphicsDeviceType.OpenGLES3 }); } else { PlayerSettings.SetGraphicsAPIs(BuildTarget.WebGL, new GraphicsDeviceType[] { GraphicsDeviceType.OpenGLES2 }); } } // 可以调用这个来集成 public void DoExport(bool buildWebGL) { OnLostFocus(); EditorUtility.SetDirty(config); AssetDatabase.SaveAssets(); if (dst == "") { ShowNotification(new GUIContent("请先选择游戏导出路径")); } else { #if UNITY_EDITOR_OSX // MacSetAuth(); #endif //仅删除StreamingAssets目录 if (deleteStreamingAssets) { UnityUtil.DelectDir(Path.Combine(dst, webglDir + "/StreamingAssets")); } if (buildWebGL && Build() != 0) { return; } ConvertCode(); int res = GenerateBinFile(); if (res == 0) { checkNeedCopyDataPackage(false); UnityEngine.Debug.LogFormat("[Converter] All done!"); ShowNotification(new GUIContent("转换完成")); } else { checkNeedCopyDataPackage(true); } // 如果是2021版本,官方symbols产生有BUG,这里需要用工具将embedded的函数名提取出来 #if UNITY_2021_2_OR_NEWER var path = "Assets/WX-WASM-SDK/Editor/Node"; var nodePath = "node"; #if UNITY_EDITOR_OSX nodePath = "/usr/local/bin/node"; #endif WeChatWASM.UnityUtil.RunCmd(nodePath, string.Format($"--experimental-modules dump_wasm_symbol.mjs {dst}"), path); UnityEngine.Debug.LogError($"Unity 2021版本使用Embeded Symbols, 代码包中含有函数名体积较大, 发布前使用代码分包工具进行优化"); #endif } } } }