Add the WeChat minigame SDK and a base-package export that leaves gameplay on the CDN.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
udx
2026-10-22 09:24:59 +08:00
co-authored by Cursor
parent 079313fb06
commit 6ebc9401ce
87 changed files with 4969 additions and 38 deletions
@@ -0,0 +1,202 @@
#if UNITY_EDITOR
using System.IO;
using System.Linq;
using System.Text;
using HybridCLR.Editor.Installer;
using UnityEditor;
using UnityEngine;
using WeChatWASM;
namespace AOT.Editor.WeChat
{
/// <summary>
/// 导出微信小游戏基础包:只含 Boot 壳。首包资源走 CDN,玩法与美术仍由 YooAsset 热更。
/// </summary>
public static class ExportWeChatBasePackage
{
const string BootScene = "Assets/Scenes/Boot.unity";
const string RelativeDst = "Build/WeChat/Base";
const string DataCdn = "https://www.xworld.ren/wechat/1.0.0/minigame/";
const int ShellHeapMb = 496;
const string ResumeKey = "AIGame.WeChat.ExportBase.Resume";
[MenuItem("AIGame/WeChat/Export Base Package")]
public static string Export()
{
if (!BuildPipeline.IsBuildTargetSupported(BuildTargetGroup.WebGL, BuildTarget.WebGL))
return Fail("未安装 Unity WebGL 模块");
var installer = new InstallerController();
if (!installer.HasInstalledHybridCLR())
return Fail("HybridCLR 未初始化,先执行菜单 HybridCLR/Installer");
if (installer.PackageVersion != installer.InstalledLibil2cppVersion)
return Fail("HybridCLR 安装版本与包版本不一致,先重新执行 HybridCLR/Installer");
string sceneError = ValidateBootOnly();
if (sceneError != null)
return Fail(sceneError);
string configError = ApplyBaseConfig();
if (configError != null)
return Fail(configError);
if (EditorUserBuildSettings.activeBuildTarget != BuildTarget.WebGL)
{
SessionState.SetBool(ResumeKey, true);
bool switched = EditorUserBuildSettings.SwitchActiveBuildTargetAsync(BuildTargetGroup.WebGL, BuildTarget.WebGL);
if (!switched)
{
SessionState.EraseBool(ResumeKey);
return Fail("无法切换到 WebGL");
}
Debug.Log("[AIGame] 正在切换到 WebGL,域重载后继续导出基础包");
return "ok switching target=WebGL";
}
return ExportCore();
}
[InitializeOnLoadMethod]
static void ResumeAfterTargetSwitch()
{
EditorApplication.delayCall += ResumeIfRequested;
}
static void ResumeIfRequested()
{
if (!SessionState.GetBool(ResumeKey, false))
return;
if (EditorApplication.isCompiling || EditorApplication.isUpdating)
{
EditorApplication.delayCall += ResumeIfRequested;
return;
}
if (EditorUserBuildSettings.activeBuildTarget != BuildTarget.WebGL)
{
SessionState.EraseBool(ResumeKey);
Debug.LogError("[AIGame] fail: 切换 WebGL 后目标不是 WebGL");
return;
}
SessionState.EraseBool(ResumeKey);
ExportCore();
}
static string ExportCore()
{
string configError = ApplyBaseConfig();
if (configError != null)
return Fail(configError);
var error = WXConvertCore.DoExport(true);
if (error != WXConvertCore.WXExportError.SUCCEED)
return Fail("微信转换失败 " + error + ",详见 Console");
string dst = Path.GetFullPath(Path.Combine(Application.dataPath, "..", RelativeDst));
string summary = WriteReport(dst);
string minigame = Path.Combine(dst, "minigame");
string result = "ok minigame=" + minigame + " " + summary;
Debug.Log("[AIGame] " + result);
return result;
}
static string ValidateBootOnly()
{
if (!File.Exists(BootScene))
return "缺少场景 " + BootScene;
var enabled = EditorBuildSettings.scenes.Where(scene => scene.enabled).Select(scene => scene.path).ToArray();
if (enabled.Length != 1 || enabled[0] != BootScene)
return "基础包只能包含 " + BootScene + ",当前启用场景: " + string.Join(", ", enabled);
return null;
}
static string ApplyBaseConfig()
{
var config = WXConvertCore.config;
if (config == null)
return "找不到 MiniGameConfig";
string dst = Path.GetFullPath(Path.Combine(Application.dataPath, "..", RelativeDst));
var so = new SerializedObject(config);
var project = so.FindProperty("ProjectConf");
var compile = so.FindProperty("CompileOptions");
var sdk = so.FindProperty("SDKOptions");
if (project == null || compile == null || sdk == null)
return "MiniGameConfig 字段不完整";
project.FindPropertyRelative("projectName").stringValue = PlayerSettings.productName;
project.FindPropertyRelative("relativeDST").stringValue = RelativeDst;
project.FindPropertyRelative("DST").stringValue = dst;
project.FindPropertyRelative("CDN").stringValue = DataCdn;
project.FindPropertyRelative("assetLoadType").intValue = 0;
project.FindPropertyRelative("Orientation").enumValueIndex = 0;
project.FindPropertyRelative("MemorySize").intValue = ShellHeapMb;
project.FindPropertyRelative("compressDataPackage").boolValue = false;
compile.FindPropertyRelative("DevelopBuild").boolValue = false;
compile.FindPropertyRelative("Il2CppOptimizeSize").boolValue = false;
compile.FindPropertyRelative("Webgl2").boolValue = true;
compile.FindPropertyRelative("fbslim").boolValue = true;
compile.FindPropertyRelative("DeleteStreamingAssets").boolValue = false;
sdk.FindPropertyRelative("UseFriendRelation").boolValue = false;
so.ApplyModifiedPropertiesWithoutUndo();
EditorUtility.SetDirty(config);
AssetDatabase.SaveAssets();
if (PlayerSettings.defaultInterfaceOrientation != UIOrientation.Portrait)
{
PlayerSettings.defaultInterfaceOrientation = UIOrientation.Portrait;
AssetDatabase.SaveAssets();
}
return null;
}
static string WriteReport(string dst)
{
string mini = Path.Combine(dst, "minigame");
string data = Path.Combine(dst, "webgl", "Build", "webgl.data");
long miniBytes = Directory.Exists(mini) ? DirectorySize(mini) : 0;
long dataBytes = File.Exists(data) ? new FileInfo(data).Length : 0;
var text = new StringBuilder();
text.AppendLine("AIGame WeChat base package");
text.AppendLine("scenes: " + BootScene);
text.AppendLine("dataCdn: " + DataCdn);
text.AppendLine("unityHeapMB: " + ShellHeapMb);
text.AppendLine("minigameBytes: " + miniBytes);
text.AppendLine("dataBytes: " + dataBytes);
text.AppendLine("devtools: " + mini);
if (Directory.Exists(mini))
{
text.AppendLine("largest:");
foreach (var file in new DirectoryInfo(mini).GetFiles("*", SearchOption.AllDirectories)
.OrderByDescending(item => item.Length)
.Take(12))
{
text.AppendLine(file.Length + "\t" + file.FullName.Substring(mini.Length).TrimStart('\\', '/'));
}
}
Directory.CreateDirectory(dst);
string report = Path.Combine(dst, "base-package-report.txt");
File.WriteAllText(report, text.ToString());
return "minigameBytes=" + miniBytes + " dataBytes=" + dataBytes + " report=" + report;
}
static long DirectorySize(string path)
{
long total = 0;
foreach (var file in Directory.GetFiles(path, "*", SearchOption.AllDirectories))
total += new FileInfo(file).Length;
return total;
}
static string Fail(string reason)
{
Debug.LogError("[AIGame] " + reason);
return "fail: " + reason;
}
}
}
#endif
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 22f48bd337e809047a1a493771575b5d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: