Add hot-update boot pipeline, UI bundles/atlas assets, and design docs.

Ignore AIClient/Library and AIClient/HybridCLRData permanently; include HybridCLR/YooAsset project setup and UI tooling.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
udx
2026-09-21 16:02:58 +08:00
co-authored by Cursor
parent f3f66ab063
commit b7a0bbcb89
279 changed files with 27332 additions and 14 deletions
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: ddfb40b9e8aa2134f8393d62252209e1
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,41 @@
#if UNITY_EDITOR
using HybridCLR.Editor;
using HybridCLR.Editor.Settings;
using UnityEditor;
using UnityEngine;
namespace AOT.Editor.HybridCLR
{
/// <summary>
/// 将 HotUpdate.Runtime 登记为 HybridCLR 热更程序集。
/// </summary>
public static class ConfigureHotUpdateAssemblies
{
const string HotUpdateAsmdef = "Assets/HotUpdate/Scripts/HotUpdate.Runtime.asmdef";
[MenuItem("AIGame/HybridCLR/Configure HotUpdate Assemblies")]
public static string Configure()
{
var asmdef = AssetDatabase.LoadAssetAtPath<UnityEditorInternal.AssemblyDefinitionAsset>(HotUpdateAsmdef);
if (asmdef == null)
return $"missing asmdef: {HotUpdateAsmdef}";
var settings = HybridCLRSettings.Instance;
settings.hotUpdateAssemblyDefinitions = new[] { asmdef };
HybridCLRSettings.Save();
AssetDatabase.SaveAssets();
return $"ok HotUpdateAssemblies=[{asmdef.name}] enable={settings.enable}";
}
public static string Status()
{
var settings = HybridCLRSettings.Instance;
int n = settings.hotUpdateAssemblyDefinitions?.Length ?? 0;
string names = n == 0
? "(none)"
: string.Join(",", System.Array.ConvertAll(settings.hotUpdateAssemblyDefinitions, a => a != null ? a.name : "null"));
return $"enable={settings.enable} count={n} defs=[{names}]";
}
}
}
#endif
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e606fddadecaef64e8894556f34e5893
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+65
View File
@@ -0,0 +1,65 @@
#if UNITY_EDITOR
using System.IO;
using UnityEditor;
using UnityEngine;
/// <summary>
/// Pipeline / 菜单:创建 Boot 场景并写入 Build Settings。
/// </summary>
public static class SetupBootScene
{
const string ScenePath = "Assets/Scenes/Boot.unity";
[MenuItem("AIGame/Boot/Setup Boot Scene")]
public static string Setup()
{
var scene = UnityEditor.SceneManagement.EditorSceneManager.NewScene(
UnityEditor.SceneManagement.NewSceneSetup.DefaultGameObjects,
UnityEditor.SceneManagement.NewSceneMode.Single);
// 竖屏参考:调整主相机背景
var cam = Camera.main;
if (cam != null)
{
cam.backgroundColor = new Color(0.08f, 0.09f, 0.12f, 1f);
cam.clearFlags = CameraClearFlags.SolidColor;
}
var bootGo = new GameObject("Boot");
bootGo.AddComponent<AOT.Boot.BootDriver>();
Directory.CreateDirectory("Assets/Scenes");
UnityEditor.SceneManagement.EditorSceneManager.SaveScene(scene, ScenePath);
var scenes = new[]
{
new EditorBuildSettingsScene(ScenePath, true),
};
// 保留其它场景但 Boot 置顶
var existing = EditorBuildSettings.scenes;
var list = new System.Collections.Generic.List<EditorBuildSettingsScene> { scenes[0] };
foreach (var s in existing)
{
if (s.path != ScenePath)
list.Add(s);
}
EditorBuildSettings.scenes = list.ToArray();
AssetDatabase.SaveAssets();
return $"ok scene={ScenePath} buildScenes={EditorBuildSettings.scenes.Length}";
}
public static string EnsurePortrait()
{
PlayerSettings.defaultInterfaceOrientation = UIOrientation.Portrait;
PlayerSettings.allowedAutorotateToPortrait = true;
PlayerSettings.allowedAutorotateToPortraitUpsideDown = false;
PlayerSettings.allowedAutorotateToLandscapeLeft = false;
PlayerSettings.allowedAutorotateToLandscapeRight = false;
PlayerSettings.companyName = "XWorld";
PlayerSettings.productName = "AIGame";
PlayerSettings.bundleVersion = "1.0.0";
return $"orientation={PlayerSettings.defaultInterfaceOrientation} product={PlayerSettings.productName} ver={PlayerSettings.bundleVersion}";
}
}
#endif
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d499aef8485eca1498241f05e2742d6a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 8be9d8bbcab6e504da0a1afb4d7aba6f
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,105 @@
#if UNITY_EDITOR
using System;
using UnityEditor;
using UnityEngine;
namespace AIGame.EditorTools
{
/// <summary>
/// Unity 获得焦点时自动转换 Doc/UIPrefabCreater 中变更的 UI JSON。
/// </summary>
[InitializeOnLoad]
public static class JsonUIAutoConvert
{
private const string PrefKey = "AIGame.UI.AutoConvertJsonOnFocus";
private static bool s_Busy;
private static double s_LastTime;
static JsonUIAutoConvert()
{
EditorApplication.focusChanged += OnFocusChanged;
}
private static void OnFocusChanged(bool focused)
{
if (!focused)
{
return;
}
AutoConvertOnFocus();
}
[MenuItem("Tools/UI/Convert Changed JSON Prefabs Now", false, 200)]
public static void ConvertChangedNow()
{
ConvertChanged(false, "manual");
}
[MenuItem("Tools/UI/Auto Convert JSON On Focus", false, 202)]
public static void ToggleAutoConvert()
{
bool enabled = !IsEnabled();
EditorPrefs.SetBool(PrefKey, enabled);
Menu.SetChecked("Tools/UI/Auto Convert JSON On Focus", enabled);
Debug.Log("[UIGen] Auto convert JSON on focus: " + (enabled ? "enabled" : "disabled"));
}
[MenuItem("Tools/UI/Auto Convert JSON On Focus", true)]
public static bool ToggleAutoConvertValidate()
{
Menu.SetChecked("Tools/UI/Auto Convert JSON On Focus", IsEnabled());
return true;
}
public static void AutoConvertOnFocus()
{
if (!IsEnabled())
{
return;
}
ConvertChanged(false, "focus");
}
private static bool IsEnabled()
{
if (EditorPrefs.HasKey(PrefKey))
{
return EditorPrefs.GetBool(PrefKey, true);
}
return true;
}
private static void ConvertChanged(bool force, string reason)
{
if (s_Busy)
{
return;
}
if (!force && EditorApplication.timeSinceStartup - s_LastTime < 1.0)
{
return;
}
if (EditorApplication.isCompiling || EditorApplication.isUpdating || EditorApplication.isPlayingOrWillChangePlaymode)
{
return;
}
s_Busy = true;
s_LastTime = EditorApplication.timeSinceStartup;
try
{
string jsonRoot = JsonUIPrefabBuilder.ResolveDefaultJsonRoot();
JsonUIPrefabBuilder.BuildChanged(jsonRoot, reason, force);
}
catch (Exception ex)
{
Debug.LogException(ex);
}
finally
{
s_Busy = false;
}
}
}
}
#endif
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: bf0a5bf71c3566f4688e2aa6fdd7b230
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a7519c9fb16ec144ebe9339123b49c5b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 930ed1c075c6ef74e94f60f3981cbe3f
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,139 @@
#if UNITY_EDITOR
using System;
using System.IO;
using HybridCLR.Editor;
using HybridCLR.Editor.Commands;
using UnityEditor;
using UnityEngine;
using YooAsset;
using YooAsset.Editor;
namespace AOT.Editor.YooAssetTools
{
/// <summary>
/// CompileDll → 拷贝到 Bundles/Raw → RawFile 构建 → 发布到 Tools/cdn_root + StreamingAssets。
/// </summary>
public static class BuildAndStageHotUpdate
{
public const string PackageName = SetupBundleCollector.PackageName;
public const string AppVersion = "1.0.0";
public const string Channel = "wechat";
static string RepoRoot => Path.GetFullPath(Path.Combine(Application.dataPath, "..", ".."));
static string CdnRoot => Path.Combine(RepoRoot, "Tools", "cdn_root");
static string ProjectBundlesRoot => BundleBuilderHelper.GetDefaultBuildOutputRoot();
[MenuItem("AIGame/YooAsset/Build And Stage (Raw + CDN)")]
public static string BuildAndStage()
{
SetupBundleCollector.Setup();
var target = EditorUserBuildSettings.activeBuildTarget;
CompileDllCommand.CompileDll(target, false);
var dllSrc = Path.Combine(SettingsUtil.GetHotUpdateDllsOutputDirByTarget(target), "HotUpdate.Runtime.dll");
if (!File.Exists(dllSrc))
return $"fail: missing compiled dll: {dllSrc}";
Directory.CreateDirectory(Path.GetDirectoryName(SetupBundleCollector.DllBytesPath) ?? "Assets/Bundles/Raw");
File.Copy(dllSrc, SetupBundleCollector.DllBytesPath, overwrite: true);
AssetDatabase.ImportAsset(SetupBundleCollector.DllBytesPath, ImportAssetOptions.ForceUpdate);
string packageVersion = DateTime.Now.ToString("yyyyMMdd.HHmmss");
var buildResult = BuildRawPackage(target, packageVersion);
if (!buildResult.Success)
return $"fail build: {buildResult.ErrorInfo}";
string packageDir = buildResult.OutputPackageDirectory;
string stageDir = Path.Combine(CdnRoot, Channel, AppVersion, PackageName);
StageDirectory(packageDir, stageDir);
string publicCdnRoot = $"https://www.xworld.ren/{Channel}/{AppVersion}/{PackageName}";
string localCdnRoot = $"http://127.0.0.1:8787/{Channel}/{AppVersion}/{PackageName}";
// 本地 serve 用 local;上传前用 Tools/cdn_publish.py write-version 切到 public
WriteVersionJson(Path.Combine(CdnRoot, Channel, "version.json"), packageVersion, localCdnRoot);
WriteVersionJson(Path.Combine(CdnRoot, Channel, "version.prod.json"), packageVersion, publicCdnRoot);
WriteVersionJson(Path.Combine(Application.streamingAssetsPath, "cdn", "version.json"), packageVersion, localCdnRoot);
WritePublishMeta(packageVersion, packageDir, stageDir, dllSrc);
AssetDatabase.Refresh();
return $"ok ver={packageVersion} packageDir={packageDir} stage={stageDir} dllBytes={new FileInfo(SetupBundleCollector.DllBytesPath).Length}";
}
static BuildResult BuildRawPackage(BuildTarget target, string packageVersion)
{
var buildParameters = new RawFileBuildParameters
{
BuildOutputRoot = ProjectBundlesRoot,
BundledFileRoot = BundleBuilderHelper.GetStreamingAssetsRoot(),
BuildPipeline = EBuildPipeline.RawFileBuildPipeline.ToString(),
BuildBundleType = (int)EBundleType.RawBundle,
BuildTarget = target,
PackageName = PackageName,
PackageVersion = packageVersion,
VerifyBuildingResult = true,
FileNameStyle = EFileNameStyle.HashName,
// 拷贝到 StreamingAssets/yoo,便于 OfflinePlayMode 冒烟
BundledCopyOption = EBundledCopyOption.ClearAndCopyAll,
BundledCopyParams = string.Empty,
ClearBuildCacheFiles = false,
UseAssetDependencyDB = true,
};
var pipeline = new RawFileBuildPipeline();
return pipeline.Run(buildParameters, true);
}
static void StageDirectory(string src, string dst)
{
if (Directory.Exists(dst))
Directory.Delete(dst, true);
Directory.CreateDirectory(dst);
foreach (var file in Directory.GetFiles(src, "*", SearchOption.AllDirectories))
{
var rel = file.Substring(src.Length).TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
var destFile = Path.Combine(dst, rel);
Directory.CreateDirectory(Path.GetDirectoryName(destFile) ?? dst);
File.Copy(file, destFile, true);
}
}
static void WriteVersionJson(string path, string resVersion, string cdnRoot)
{
Directory.CreateDirectory(Path.GetDirectoryName(path) ?? ".");
// JsonUtility 不支持 string[],手写 JSON
var json =
"{\n" +
$" \"appVersionMin\": \"{AppVersion}\",\n" +
$" \"appVersionMax\": \"{AppVersion}\",\n" +
$" \"resVersion\": \"{resVersion}\",\n" +
$" \"cdnRoot\": \"{cdnRoot}\",\n" +
$" \"packages\": [ \"{PackageName}\" ],\n" +
" \"forceUpdate\": false,\n" +
" \"notice\": \"\"\n" +
"}\n";
File.WriteAllText(path, json);
}
static void WritePublishMeta(string resVersion, string packageDir, string stageDir, string dllSrc)
{
var metaPath = Path.Combine(CdnRoot, Channel, "last_publish.json");
Directory.CreateDirectory(Path.GetDirectoryName(metaPath) ?? CdnRoot);
var json =
"{\n" +
$" \"resVersion\": \"{resVersion}\",\n" +
$" \"appVersion\": \"{AppVersion}\",\n" +
$" \"packageDir\": \"{Escape(packageDir)}\",\n" +
$" \"stageDir\": \"{Escape(stageDir)}\",\n" +
$" \"dllSrc\": \"{Escape(dllSrc)}\",\n" +
$" \"builtAt\": \"{DateTime.Now:o}\"\n" +
"}\n";
File.WriteAllText(metaPath, json);
}
static string Escape(string path) => (path ?? string.Empty).Replace("\\", "\\\\").Replace("\"", "\\\"");
}
}
#endif
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b90533025225c0149a438a078fe71011
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,56 @@
#if UNITY_EDITOR
using AOT.Boot;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.SceneManagement;
using YooAsset;
namespace AOT.Editor.YooAssetTools
{
/// <summary>
/// 切换 BootDriver 的 M1 验证模式(Offline / Host / Direct)。
/// </summary>
public static class ConfigureBootPlayMode
{
const string BootScene = "Assets/Scenes/Boot.unity";
[MenuItem("AIGame/Boot/Mode/Editor Direct (M0)")]
public static string SetDirect() => Apply(
EPlayMode.EditorSimulateMode,
editorDirect: true,
versionUrl: "https://www.xworld.ren/wechat/version.json",
defaultCdn: "https://www.xworld.ren/wechat/1.0.0/DefaultPackage");
[MenuItem("AIGame/Boot/Mode/Offline Smoke (M1)")]
public static string SetOffline() => Apply(EPlayMode.OfflinePlayMode, editorDirect: false);
[MenuItem("AIGame/Boot/Mode/Host Local CDN (M1)")]
public static string SetHostLocal() => Apply(EPlayMode.HostPlayMode, editorDirect: false,
versionUrl: "http://127.0.0.1:8787/wechat/version.json",
defaultCdn: "http://127.0.0.1:8787/wechat/1.0.0/DefaultPackage");
static string Apply(EPlayMode playMode, bool editorDirect, string versionUrl = null, string defaultCdn = null)
{
var scene = EditorSceneManager.OpenScene(BootScene, OpenSceneMode.Single);
var boot = Object.FindObjectOfType<BootDriver>();
if (boot == null)
return "fail: BootDriver not found in Boot scene";
var so = new SerializedObject(boot);
var settings = so.FindProperty("settings");
settings.FindPropertyRelative("PlayMode").enumValueIndex = (int)playMode;
settings.FindPropertyRelative("EditorDirectHotUpdate").boolValue = editorDirect;
if (!string.IsNullOrEmpty(versionUrl))
settings.FindPropertyRelative("VersionUrl").stringValue = versionUrl;
if (!string.IsNullOrEmpty(defaultCdn))
settings.FindPropertyRelative("DefaultCdnRoot").stringValue = defaultCdn;
so.ApplyModifiedPropertiesWithoutUndo();
EditorSceneManager.MarkSceneDirty(scene);
EditorSceneManager.SaveScene(scene);
return $"ok playMode={playMode} editorDirect={editorDirect} versionUrl={settings.FindPropertyRelative("VersionUrl").stringValue}";
}
}
}
#endif
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 63c6c0b31c0ce5040a28cb40125ae1ae
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,93 @@
#if UNITY_EDITOR
using System.IO;
using System.Linq;
using UnityEditor;
using UnityEngine;
using YooAsset.Editor;
namespace AOT.Editor.YooAssetTools
{
/// <summary>
/// 初始化 DefaultPackage 收集配置(Raw 热更 DLL)。
/// </summary>
public static class SetupBundleCollector
{
public const string PackageName = "DefaultPackage";
public const string CollectPath = "Assets/Bundles/Raw";
public const string DllBytesPath = "Assets/Bundles/Raw/HotUpdate.Runtime.bytes";
[MenuItem("AIGame/YooAsset/Setup Bundle Collector")]
public static string Setup()
{
Directory.CreateDirectory(CollectPath);
EnsurePlaceholderDllBytes();
var setting = BundleCollectorSettingData.Setting;
setting.ShowPackageView = true;
setting.UniqueBundleName = false;
var package = setting.Packages.FirstOrDefault(p => p.PackageName == PackageName);
if (package == null)
package = BundleCollectorSettingData.CreatePackage(PackageName);
package.PackageDesc = "启动必需:热更 DLL 等";
package.EnableAddressable = true;
package.SupportExtensionless = true;
package.LocationToLower = false;
package.IncludeAssetGUID = false;
package.AutoCollectShaders = false;
package.IgnoreRuleName = nameof(RawFileIgnoreRule);
BundleCollectorSettingData.ModifyPackage(package);
var group = package.Groups.FirstOrDefault(g => g.GroupName == "HotUpdateDll");
if (group == null)
group = BundleCollectorSettingData.CreateGroup(package, "HotUpdateDll");
group.GroupDesc = "HybridCLR 热更程序集";
group.ActiveRuleName = nameof(EnableGroup);
group.AssetTags = "aot";
BundleCollectorSettingData.ModifyGroup(package, group);
var collector = group.Collectors.FirstOrDefault(c => c.CollectPath == CollectPath);
if (collector == null)
{
collector = new BundleCollector
{
CollectPath = CollectPath,
CollectorGUID = AssetDatabase.AssetPathToGUID(CollectPath),
CollectorType = ECollectorType.MainAssetCollector,
AddressRuleName = nameof(AddressByFileName),
PackRuleName = nameof(PackRawFile),
FilterRuleName = nameof(CollectAll),
AssetTags = "aot",
};
BundleCollectorSettingData.CreateCollector(group, collector);
}
else
{
collector.CollectorGUID = AssetDatabase.AssetPathToGUID(CollectPath);
collector.CollectorType = ECollectorType.MainAssetCollector;
collector.AddressRuleName = nameof(AddressByFileName);
collector.PackRuleName = nameof(PackRawFile);
collector.FilterRuleName = nameof(CollectAll);
collector.AssetTags = "aot";
BundleCollectorSettingData.ModifyCollector(group, collector);
}
package.CheckConfigError();
BundleCollectorSettingData.SaveFile();
return $"ok package={PackageName} collect={CollectPath} dll={DllBytesPath} setting={AssetDatabase.GetAssetPath(setting)}";
}
public static void EnsurePlaceholderDllBytes()
{
if (File.Exists(DllBytesPath))
return;
Directory.CreateDirectory(Path.GetDirectoryName(DllBytesPath) ?? CollectPath);
// 占位:真实内容由 BuildAndStageHotUpdate 覆盖
File.WriteAllBytes(DllBytesPath, System.Text.Encoding.UTF8.GetBytes("PLACEHOLDER_HOTUPDATE_DLL"));
AssetDatabase.ImportAsset(DllBytesPath, ImportAssetOptions.ForceUpdate);
}
}
}
#endif
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 011b17d87b02c034abdc5cad22378c26
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: