UIFramework

UIFramework 实现细节与使用说明

本文档梳理 Assets/MMOGame/Scripts/Framework/UIFramework 的实现机制,以及它和当前项目业务 UI 的关系。

需要先明确一点:项目里存在一套通用的 UIFramework,但当前业务 UI 主线大量使用的是另一套更轻的 UIRoot + UIControllerBase + ViewBase + WindowBase。两者思想相近,但调用链不同。新增业务 UI 时通常应优先沿用现有业务链路;只有准备统一改造 UI 管理方式时,才考虑让通用 UIFramework 接管窗口/面板。

目录与核心类

通用 UI 框架位于:

text 复制代码
Assets/MMOGame/Scripts/Framework/UIFramework

核心结构:

text 复制代码
UIFrame
├── PanelUILayer     管理面板 Panel,适合常驻 UI:血条、小地图、HUD
└── WindowUILayer    管理窗口 Window,适合互斥窗口、队列窗口、弹窗

主要类职责:

职责
UIFrame UI 框架门面,对外提供显示、隐藏、注册接口
UILayer<TScreen> 基础 Layer,维护 screenId -> controller 注册表
PanelUILayer 面板层,直接显示/隐藏,可多面板同时存在
WindowUILayer 窗口层,维护当前窗口、队列、历史栈、弹窗遮罩和转场阻断
UIScreenController<TProps> 所有框架 UI 控制器的基类,统一属性、动画、生命周期
WindowController 窗口控制器基类,提供窗口行为属性和关闭请求
PanelController 面板控制器基类,提供面板优先级
AniComponent UI 动画抽象,FadeAniAnimationView 是已有实现
UISettings ScriptableObject,用于从 UIFrame prefab 和 screen prefab 列表创建 UI 实例

UIFrame:框架入口

UIFrame 挂在 UI 根节点上,初始化时会从子节点中查找:

csharp 复制代码
panelLayer = gameObject.GetComponentInChildren<PanelUILayer>(true);
windowLayer = gameObject.GetComponentInChildren<WindowUILayer>(true);

初始化流程:

  1. 找到并初始化 PanelUILayer
  2. 找到并初始化 WindowUILayer
  3. 订阅窗口层的 RequestScreenBlock / RequestScreenUnblock
  4. 获取主 Canvas 的 GraphicRaycaster

窗口转场时,WindowUILayer 会通知 UIFrame 临时关闭或恢复点击:

csharp 复制代码
private void OnRequestScreenBlock() {
    graphicRaycaster.enabled = false;
}

private void OnRequestScreenUnblock() {
    graphicRaycaster.enabled = true;
}

这能避免窗口动画播放中重复点击导致状态错乱。

常用接口:

csharp 复制代码
uiFrame.ShowPanel("HpPanel");
uiFrame.HidePanel("HpPanel");

uiFrame.OpenWindow("BackpackWindow");
uiFrame.CloseWindow("BackpackWindow");
uiFrame.CloseCurrentWindow();

uiFrame.HideAll();
uiFrame.RegisterScreen("SomeWindow", controller, controller.transform);

screenId 使用字符串。注册时会写入 controller 的 ScreenId

UIScreenController:所有框架界面的生命周期基类

UIScreenController<TProps> 是窗口和面板的共同基类:

csharp 复制代码
public abstract class UIScreenController<TProps> : MonoBehaviour, IScreenController
    where TProps : IScreenProperties

它统一处理四类事情。

1. 属性系统

每个 screen 可以有序列化属性:

csharp 复制代码
[SerializeField]
private TProps properties;

显示界面时也可以传入运行时属性:

csharp 复制代码
screen.Show(properties);

Show 会检查传入属性类型是否匹配 TProps。类型不匹配时会报错并中止显示。

2. 显示/隐藏动画

每个 screen 可以配置进入动画和退出动画:

csharp 复制代码
[SerializeField] private AniComponent animIn;
[SerializeField] private AniComponent animOut;

显示时,如果配置了 animIn,会播放动画;否则直接 SetActive(true)

隐藏时,如果配置了 animOut,会播放动画;否则直接 SetActive(false)

3. 生命周期钩子

可在子类中按需重写:

csharp 复制代码
protected virtual void AddListeners() {}
protected virtual void RemoveListeners() {}
protected virtual void OnPropertiesSet() {}
protected virtual void WhileHiding() {}
protected virtual void HierarchyFixOnShow() {}

含义:

方法 调用时机 典型用途
AddListeners Awake 注册按钮点击、事件监听
RemoveListeners OnDestroy 解绑事件
OnPropertiesSet Show 设置属性之后 根据打开参数刷新 UI
WhileHiding Hide 清理临时状态
HierarchyFixOnShow Show 调整层级

WindowController 会重写 HierarchyFixOnShow,打开窗口时把自己放到最后一个 sibling:

csharp 复制代码
protected override void HierarchyFixOnShow() {
    transform.SetAsLastSibling();
}

4. 事件回调

UIScreenController 暴露以下回调给 Layer 使用:

csharp 复制代码
Action<IScreenController> InTransitionFinished;
Action<IScreenController> OutTransitionFinished;
Action<IScreenController> CloseRequest;
Action<IScreenController> ScreenDestroyed;

这些事件用于:

  • 通知转场结束。
  • 窗口请求关闭。
  • 对象销毁时自动反注册。

UILayer:注册表与按 id 操作

UILayer<TScreen> 维护一个注册表:

csharp 复制代码
protected Dictionary<string, TScreen> registeredScreens;

注册 screen:

csharp 复制代码
RegisterScreen(string screenId, TScreen controller)

内部会:

  1. 设置 controller.ScreenId = screenId
  2. 加入 registeredScreens
  3. 订阅 controller.ScreenDestroyed

销毁时会自动反注册:

csharp 复制代码
private void OnScreenDestroyed(IScreenController screen) {
    if (!string.IsNullOrEmpty(screen.ScreenId)
        && registeredScreens.ContainsKey(screen.ScreenId)) {
        UnregisterScreen(screen.ScreenId, (TScreen) screen);
    }
}

按 id 操作:

csharp 复制代码
ShowScreenById(string screenId)
ShowScreenById<TProps>(string screenId, TProps properties)
HideScreenById(string screenId)

如果 id 未注册,会直接打印错误。因此使用这套框架时,必须先注册 screen。

PanelUILayer:面板层

PanelUILayer 适合常驻或并存 UI,例如:

  • 血条
  • 小地图
  • 主界面 HUD
  • 教程提示
  • 遮挡层

它没有窗口队列和历史栈。显示/隐藏逻辑很直接:

csharp 复制代码
public override void ShowScreen(IPanelController screen) {
    screen.Show();
}

public override void HideScreen(IPanelController screen) {
    screen.Hide();
}

面板优先级

面板优先级定义:

csharp 复制代码
public enum PanelPriority {
    None = 0,
    Prioritary = 1,
    Tutorial = 2,
    Blocker = 3,
}

PanelUILayer.ReparentScreen 会根据面板的 Priority 把它放到不同父节点:

csharp 复制代码
ReparentToParaLayer(ctl.Priority, screenTransform);

这些父节点由 PanelPriorityLayerList 配置。渲染顺序取决于这些父节点在 Hierarchy 中的顺序。

WindowUILayer:窗口层

WindowUILayer 负责更复杂的窗口管理,它维护:

csharp 复制代码
public IWindowController CurrentWindow { get; private set; }

private Queue<WindowHistoryEntry> windowQueue;
private Stack<WindowHistoryEntry> windowHistory;
private HashSet<IScreenController> screensTransitioning;

含义:

字段 含义
CurrentWindow 当前前台窗口
windowQueue 等待当前窗口关闭后显示的窗口队列
windowHistory 窗口历史栈,当前窗口关闭后可回到上一个窗口
screensTransitioning 正在播放转场动画的窗口集合,用于阻断点击

打开窗口流程

调用:

csharp 复制代码
uiFrame.OpenWindow("SomeWindow");

最终进入:

csharp 复制代码
WindowUILayer.ShowScreen<TProp>(IWindowController screen, TProp properties)

流程:

text 复制代码
把传入 properties 转成 IWindowProperties
判断是否应该入队
    是:加入 windowQueue
    否:立即 DoShow

是否入队由 ShouldEnqueue 决定:

text 复制代码
如果当前没有窗口,也没有排队窗口
    直接显示

如果运行时 properties.SuppressPrefabProperties == true
    用运行时 properties.WindowQueuePriority 判断

否则
    用 prefab 上 controller.WindowPriority 判断

不是 ForceForeground
    入队
否则
    直接显示

窗口优先级:

csharp 复制代码
public enum WindowPriority {
    ForceForeground = 0,
    Enqueue = 1,
}
  • ForceForeground:立即显示到前台。
  • Enqueue:如果已有窗口,排队等待。

DoShow 流程

真正显示窗口在 DoShow 中:

text 复制代码
如果当前窗口就是要打开的窗口
    打 warning

否则如果当前窗口存在,并且当前窗口 HideOnForegroundLost == true,并且新窗口不是 popup
    隐藏当前窗口

把新窗口压入 windowHistory
记录转场 AddTransition
如果新窗口 IsPopup
    显示黑色遮罩
调用 windowEntry.Show()
CurrentWindow = 新窗口

这意味着窗口层默认是"当前窗口优先"的模型,而不是任意多个普通窗口并存的模型。

关闭窗口流程

关闭窗口最终进入:

csharp 复制代码
WindowUILayer.HideScreen(IWindowController screen)

它只允许关闭当前窗口:

text 复制代码
如果 screen == CurrentWindow
    从 windowHistory 弹出当前窗口
    播放隐藏动画
    CurrentWindow = null

    如果 windowQueue 有窗口
        显示队列下一个
    否则如果 windowHistory 还有窗口
        回到上一个历史窗口
否则
    打错误日志并忽略

所以不要用 WindowUILayer 随意关闭后台窗口。它的模型是:只关闭前台窗口,然后恢复队列或历史窗口。

WindowProperties:窗口行为属性

窗口属性定义在 WindowProperties

csharp 复制代码
public class WindowProperties : IWindowProperties

关键字段:

字段 含义
HideOnForegroundLost 当前窗口被其他非 popup 窗口置前时,自己是否隐藏
WindowQueuePriority 打开策略:立即前台或排队
IsPopup 是否弹窗,弹窗会进入优先窗口层并显示黑色遮罩
SuppressPrefabProperties 运行时属性是否覆盖 prefab 上配置的窗口行为

SuppressPrefabProperties 容易误解。WindowController.SetProperties 的逻辑是:

csharp 复制代码
if (!props.SuppressPrefabProperties) {
    props.HideOnForegroundLost = Properties.HideOnForegroundLost;
    props.WindowQueuePriority = Properties.WindowQueuePriority;
    props.IsPopup = Properties.IsPopup;
}

Properties = props;

结论:

  • SuppressPrefabProperties == false:保留 prefab 上配置的窗口行为,只用运行时属性携带业务数据。
  • SuppressPrefabProperties == true:运行时传入的属性可以覆盖 HideOnForegroundLostWindowQueuePriorityIsPopup

可以把它理解为"是否压制/忽略 prefab 上的属性"。

弹窗遮罩 WindowParaLayer

WindowParaLayer 是弹窗的优先层。它负责:

  • 接收 IsPopup == true 的窗口。
  • 管理黑色背景 darkenBgObject
  • 有 popup 激活时显示遮罩。
  • 没有 popup 激活时关闭遮罩。

注册 screen 时,如果窗口是 popup:

csharp 复制代码
priorityParaLayer.AddScreen(screenTransform);

打开 popup 时:

csharp 复制代码
priorityParaLayer.DarkenBG();

隐藏动画结束后:

csharp 复制代码
priorityParaLayer.RefreshDarken();

遮罩不是窗口自己控制的,而是 WindowUILayer + WindowParaLayer 统一控制。

动画系统

动画抽象:

csharp 复制代码
public abstract class AniComponent : MonoBehaviour {
    public abstract void Animate(Transform target, Action callWhenFinished);
}

已有实现:

实现方式
FadeAni 通过 CanvasGroup.alpha 做淡入/淡出
AnimationView 使用 Unity Legacy Animation 播放 AnimationClip,支持反向播放

如果 screen 没有配置动画组件,UIScreenController 会直接切换激活状态。

注意:FadeAni 在动画结束后会把 canvasGroup.alpha 设置成 1f,即使是淡出也是这样。由于隐藏流程随后会 SetActive(false),通常不影响表现,但复用该组件时需要知道这个行为。

UISettings 与编辑器工具

UISettings 是一个 ScriptableObject:

csharp 复制代码
[CreateAssetMenu(fileName = "UISettings", menuName = "UI/UI Settings")]

配置项:

字段 含义
templateUIPrefab UIFrame 预制体
screensToRegister 要注册的窗口/面板 prefab 列表
deactivateScreenGOs 实例化后是否隐藏 screen GameObject

调用:

csharp 复制代码
UIFrame frame = uiSettings.CreateUIInstance();

内部流程:

text 复制代码
实例化 UIFrame prefab
遍历 screensToRegister
    实例化 screen prefab
    获取 IScreenController
    用 prefab 名称作为 screenId 注册
    根据配置决定是否 SetActive(false)

编辑器工具菜单:

text 复制代码
Assets/Create/UI/UI Frame in Scene
Assets/Create/UI/UI Frame Prefab

它会创建如下结构:

text 复制代码
UIFrame
├── UICamera
├── EventSystem
├── PanelLayer
├── WindowLayer
├── PriorityPanelLayer
├── PriorityWindowLayer
│   └── DarkenBG
└── TutorialPanelLayer

Canvas 会被设置为 ScreenSpaceCamera,参考分辨率是 1920x1080

通用 UIFramework 的使用流程

1. 创建 UIFrame

可以通过菜单创建:

text 复制代码
Assets/Create/UI/UI Frame Prefab

或:

text 复制代码
Assets/Create/UI/UI Frame in Scene

2. 创建窗口或面板 prefab

窗口示例:

csharp 复制代码
using UIFramework;

public class BackpackWindowController : WindowController
{
    protected override void AddListeners()
    {
        // 注册按钮事件
    }

    protected override void RemoveListeners()
    {
        // 解绑按钮事件
    }

    protected override void OnPropertiesSet()
    {
        // 根据 Properties 刷新 UI
    }

    public void OnCloseButtonClick()
    {
        UI_Close();
    }
}

面板示例:

csharp 复制代码
using UIFramework;

public class HpPanelController : PanelController
{
    protected override void OnPropertiesSet()
    {
        // 刷新血条
    }
}

3. 配置 prefab 属性

窗口可以配置:

  • Hide On Foreground Lost
  • Window Queue Priority
  • Is Popup
  • Anim In
  • Anim Out

面板可以配置:

  • Panel Priority
  • Anim In
  • Anim Out

4. 注册 screen

手动注册:

csharp 复制代码
uiFrame.RegisterWindow("BackpackWindow", backpackController);
uiFrame.RegisterPanel("HpPanel", hpPanelController);

或统一入口:

csharp 复制代码
uiFrame.RegisterScreen("BackpackWindow", backpackController, backpackController.transform);

如果使用 UISettings,则把 prefab 填进 screensToRegister,由 CreateUIInstance() 自动实例化并注册。

5. 显示与隐藏

窗口:

csharp 复制代码
uiFrame.OpenWindow("BackpackWindow");
uiFrame.CloseWindow("BackpackWindow");
uiFrame.CloseCurrentWindow();

面板:

csharp 复制代码
uiFrame.ShowPanel("HpPanel");
uiFrame.HidePanel("HpPanel");

带属性打开窗口:

csharp 复制代码
var props = new WindowProperties {
    WindowQueuePriority = WindowPriority.Enqueue,
    HideOnForegroundLost = true,
    IsPopup = false,
    SuppressPrefabProperties = true
};

uiFrame.OpenWindow("BackpackWindow", props);

当前项目实际业务 UI 链路

当前项目业务 UI 主要走这条链路:

text 复制代码
UIRoot
└── LoginUIController / SelectRoleUIController / MainCityUIController
    └── ViewBase
        └── WindowBase

对应目录:

text 复制代码
Assets/MMOGame/Scripts/UI

核心类:

作用
UIRoot 业务 UI 根节点,加载 LoginView、SelectRoleView、MainCityView
UIControllerBase View 的控制器基类,提供 ShowWindowUpdateUI 等接口
ViewBase 维护 WindowType -> WindowBase 字典
WindowBase 具体窗口基类,默认直接 SetActiveUpdateUI

这套业务 UI 不依赖 UIFrame 注册表,不使用窗口队列、历史栈、弹窗遮罩和 AniComponent。它更简单,靠 WindowType 和字典切换窗口。

ViewBase

ViewBase 维护窗口字典:

csharp 复制代码
protected Dictionary<WindowType, WindowBase> windowDic;

初始化:

csharp 复制代码
public virtual void InitView()
{
    windowDic = new Dictionary<WindowType, WindowBase>();
}

显示某个窗口:

csharp 复制代码
public virtual void ShowWindow(WindowType windowType, object obj = null)
{
    if (windowDic == null || windowDic.Count == 0) return;
    foreach (var window in windowDic.Keys)
    {
        windowDic[window].Show(window == windowType, obj);
    }
}

这意味着基础实现会显示目标窗口,同时隐藏其他窗口。

刷新 UI:

csharp 复制代码
public virtual void UpdateUI(WindowType windowType, object obj)
{
    windowDic[windowType].UpdateUI(obj);
}

WindowBase

WindowBase 默认很轻:

csharp 复制代码
public virtual void Show(bool isShow, object obj = null)
{
    gameObject.SetActive(isShow);

    if (obj != null && isShow)
    {
        UpdateUI(obj);
    }
}

具体窗口通过重写:

csharp 复制代码
public override void InitWindow()
public override void UpdateUI(object obj)

实现自己的初始化和刷新。

UIControllerBase

UIControllerBase 包装 View:

csharp 复制代码
public virtual void ShowWindow(WindowType windowType, object obj = null)
{
    if (!_view.gameObject.activeSelf)
    {
        _view.ShowView(true);
    }
    _view.ShowWindow(windowType, obj);
}

控制器负责:

  • 注册 View 中按钮回调。
  • 注册网络协议回调。
  • 发送网络请求。
  • 收到协议返回后调用 UpdateUIShowWindow

注意:UIControllerBase.Dispose() 当前直接抛 NotImplementedException,不要在未实现前调用。

业务 UI 示例:登录界面

UIRoot.InitLoginView 加载登录 UI prefab:

csharp 复制代码
ResourceManager.Instance.LoadPrefabAsync("UIPrefabs/View/LoginView", (go) =>
{
    LoginView loginView = go.GetComponent<LoginView>();
    go.transform.SetParent(_canvas.transform, false);
    LoginUIController = new LoginUIController(loginView);
    LoginUIController.ShowWindow(WindowType.LoginWindow);
});

LoginUIController 构造时:

csharp 复制代码
_synchronizationContext = SynchronizationContext.Current;
_loginView = view as LoginView;
_loginView.InitView();
RegistCommand();

LoginView.InitView 将 Inspector 里拖入的窗口注册到字典:

csharp 复制代码
windowDic.Add(WindowType.LoginWindow, _loginWindow);
windowDic.Add(WindowType.RegistWindow, _registWindow);
windowDic.Add(WindowType.ResetPwdWindow, _resetPwdWindow);
windowDic.Add(WindowType.GameServerWindow, _gameServerWindow);
windowDic.Add(WindowType.ServerListWindow, _serverListWindow);

控制器收到登录成功后,会切换窗口:

csharp 复制代码
ShowWindow(WindowType.GameServerWindow, ret.GameServer);

业务 UI 示例:主城界面

MainCityView.InitView 注册窗口:

csharp 复制代码
windowDic.Add(WindowType.RoleInfoWindow, _roleInfoWindow);
windowDic.Add(WindowType.ChatWindow, _chatWindow);
windowDic.Add(WindowType.MinMapWindow, _minMapWindow);
windowDic.Add(WindowType.SkilIInfoWindow, _skillInfoWindow);
windowDic.Add(WindowType.BackpackWindow, _backpackWindow);
windowDic.Add(WindowType.TalkWindow, _talkWindow);
windowDic.Add(WindowType.ShopWindow, _shopWindow);
windowDic.Add(WindowType.RoleAttributeWindow, _roleAttributeWindow);
windowDic.Add(WindowType.InputWindow, _inputWindow);

主城重写了 ShowWindow,实现"点一次打开,再点一次关闭"和互斥窗口:

csharp 复制代码
if (windowDic[windowType].IsActive())
{
    CameraManager.Instance.SetCursorVisible(false);
    windowDic[windowType].Show(false);
}
else
{
    CameraManager.Instance.SetCursorVisible(true);
    windowDic[windowType].Show(true, obj);

    foreach (var item in _exclusionWindows)
    {
        if (item.WindowType == windowType)
        {
            foreach (var window in item.windowTypes)
            {
                windowDic[window].Show(false);
            }
        }
    }
}

所以主城 UI 不是基础 ViewBase 的"打开一个、隐藏全部"模式,而是允许 HUD、窗口、互斥窗口按业务规则共存。

两套 UI 的差异

项目 通用 UIFramework 当前业务 UI
核心入口 UIFrame UIRoot
screen 标识 string screenId WindowType enum
基类 UIScreenController<TProps> WindowBase / ViewBase
注册方式 RegisterScreen/RegisterWindow/RegisterPanel View.InitView() 手动 windowDic.Add
窗口队列
历史栈
弹窗遮罩 无,业务自己处理
动画系统 AniComponent 默认直接 SetActive
属性传递 强类型 IWindowProperties/IPanelProperties 弱类型 object obj
当前使用程度 框架代码存在,但业务 UI 没大量继承它 主力业务方案

新增业务 UI 的推荐做法

如果是给当前项目新增业务窗口,例如好友、任务、邮件等,建议沿用现有业务 UI 链路。

1. 增加枚举

EnumDefine.csWindowType 中增加:

csharp 复制代码
FriendWindow

2. 创建窗口脚本

csharp 复制代码
public class FriendWindow : WindowBase
{
    public override void InitWindow()
    {
        // 初始化按钮、列表等
    }

    public override void UpdateUI(object obj)
    {
        // 根据 obj 刷新好友列表
    }
}

3. 在对应 View 中声明字段

例如在 MainCityView

csharp 复制代码
[SerializeField]
private FriendWindow _friendWindow;

4. 在 InitView 注册

csharp 复制代码
windowDic.Add(WindowType.FriendWindow, _friendWindow);

5. 在 Controller 中控制显示和刷新

csharp 复制代码
ShowWindow(WindowType.FriendWindow);
UpdateUI(WindowType.FriendWindow, friendList);

6. 配置互斥关系

如果该窗口需要和其他窗口互斥,在 MainCityView_exclusionWindows Inspector 配置中增加规则。

什么时候使用通用 UIFramework

适合使用通用 UIFramework 的情况:

  • 准备重新统一 UI 管理入口。
  • 需要窗口队列、历史栈、弹窗遮罩、转场阻断这些能力。
  • 想用强类型 Properties 替代 object obj
  • 想通过 UISettings 批量注册 UI prefab。

不建议混用的情况:

  • 同一个业务模块里,一部分窗口由 UIRoot/ViewBase 管,一部分窗口由 UIFrame 管。
  • 同一个窗口既出现在 WindowType 字典中,又注册到 UIFrame 中。

混用会导致显隐状态、层级、输入阻断和生命周期入口不一致。

当前实现中的注意点

  1. UIControllerBase.Dispose() 未实现,当前调用会抛异常。

  2. ViewBase.ShowWindow(WindowType, object) 会隐藏其他所有窗口,适合登录页,但不适合主城 HUD。主城已经通过重写解决。

  3. WindowBase.UpdateUI(object obj) 是弱类型,调用方和窗口之间依赖约定。修改时要确认传入对象的真实类型。

  4. 通用 WindowUILayer.HideScreen 只能关闭当前窗口,不能随意关闭后台窗口。

  5. 通用 WindowProperties.SuppressPrefabProperties 会影响运行时属性和 prefab 属性的优先级,使用前要确认期望行为。

  6. FadeAni 淡出完成后会把 alpha 设回 1f,通常因为对象被隐藏而无影响,但复用时要注意。

结论

项目中有完整的通用 UIFramework:它支持 screen 注册、Panel/Window 分层、窗口队列、历史栈、弹窗遮罩、属性传递和动画转场。

但当前 MMORPG 业务 UI 的主线是:

text 复制代码
UIRoot -> UIControllerBase -> ViewBase -> WindowBase

控制器负责网络消息和业务回调,View 负责窗口集合和互斥显示,Window 负责具体界面刷新。

新增普通业务 UI 时,优先沿用当前业务链路;只有当需求明确需要队列、历史栈、弹窗遮罩或强类型属性系统时,再考虑接入或改造通用 UIFramework

相关推荐
互联网散修1 小时前
鸿蒙实战:图片编辑器——添加文字的UI适配与键盘避让
ui·编辑器·harmonyos
YJlio2 小时前
OpenClaw v2026.5.26-beta.1 / beta.2 预发布解读:Gateway 加速、transcript 路径统一、多通道修复、语音增强与安装更新链路加固
人工智能·windows·python·ui·缓存·gateway·outlook
℡枫叶℡11 小时前
Unity - Import Activity Window 资源导入诊断信息窗口
unity·资源导入诊断
TO_ZRG13 小时前
Unity 证书校验
unity·游戏引擎
mxwin15 小时前
Unity Shader 切线空间数据是如何计算出来的
unity·游戏引擎·shader
mxwin18 小时前
Unity Shader 法线贴图跟切线空间有什么关系
unity·游戏引擎·贴图·shader
mxwin19 小时前
Unity Shader 贴图和采样的关系 如何保证贴图清晰
unity·游戏引擎·贴图·shader
Roc-xb19 小时前
hermes-web-ui安装教程
前端·ui·hermes-web-ui