Chapter 202 · 综合实战

应用骨架:场景 + UI 共存

游戏循环嵌入 XUI:场景渲染与 UI 渲染分层、输入路由、帧循环整合。

202

概念

游戏/应用中 XGE 场景与 XUI 界面共存:
• 渲染顺序:先绘制游戏场景,再绘制 UI 覆盖层
• 输入路由:UI 优先消费输入,未消费传递给游戏
• 帧循环:Update → GameRender → UI Update → UI Render
• 场景栈:xgeSceneSet 管理游戏场景,XUI 独立于场景
• 暂停:UI 模态弹出时暂停游戏逻辑

帧循环架构

阶段操作
InputxuiDispatchEvent → 游戏输入
GameUpdate物理/AI/动画(UI 模态时跳过)
GameRenderxgeClear → 场景绘制
UIUpdatexuiDispatchPendingEvents
UIRenderXUI 控件树渲染(覆盖游戏画面)
PresentxgePresent

编译本章示例

gcc -O2 -I. -o ch202.exe ch202_main.c src\xui_*.c build\xge.lib -lm -lws2_32 -liphlpapi -lgdi32 -luser32 -lshell32 -lopengl32 -lole32 -lwinmm -lavrt
.\ch202.exe

示例:完整帧循环

void OnFrame(void) {
    float fDT = xgeGetDelta();

    // 1. 输入 → XUI 优先
    int bConsumed = DispatchInputToUI();

    // 2. 游戏逻辑(UI 模态时暂停)
    if (!ModalActive() /* 用 xuiPopupIsModal/xuiMsgBoxIsModal 按实例查询 */) {
        GameUpdate(fDT);
    }

    // 3. 游戏渲染
    xgeClear(0x0A0A1AFF);
    GameRender();

    // 4. XUI 更新 + 渲染
    xuiDispatchPendingEvents(pCtx);
    xuiUpdate(pCtx, fDT);
    xuiLayout(pCtx);  /* 绘制由根控件经代理完成 */

    // 5. 未消费输入 → 游戏
    if (!bConsumed)
        GameInput();
}

完整代码

将以下代码保存为 ch202_main.c,编译后运行即可看到游戏渲染 + XUI 覆盖的帧循环架构:

#include "xge.h"
#include "xui.h"

static xui_context g_pCtx;
static float g_fAngle = 0.0f;

static int onFrame(void* pUser) {
    (void)pUser;
    float dt = xgeGetDelta();

    /* 1. 游戏逻辑 */
    g_fAngle += dt * 90.0f;

    /* 2. 游戏渲染 */
    xgeClear(0x0a0a1a);
    xgeShapeRectFill(270, 190, 100, 100, 0x4FD8C2FF);

    /* 3. XUI 输入 + 更新 + 渲染(覆盖游戏画面) */
    xui_vec2_t pos;
    xgeMouseGet(&pos.x, &pos.y);
    xuiInputPointerMove(g_pCtx, pos.x, pos.y);
    if (xgeMouseDown(XGE_MOUSE_LEFT))
        xuiInputPointerDown(g_pCtx, pos.x, pos.y, 0);
    else
        xuiInputPointerUp(g_pCtx, pos.x, pos.y, 0);
    xuiUpdate(g_pCtx, dt);
    xuiLayout(g_pCtx);
    xuiRender(g_pCtx, NULL, NULL, 0);
    return XGE_OK;
}

int main(void) {
    xge_desc_t d = {0};
    d.iWidth = 640; d.iHeight = 480;
    d.sTitle = "ch202 - Game + UI Loop";
    d.iFlags = XGE_INIT_VSYNC;
    xgeInit(&d);
    xuiCreate(&g_pCtx);
    extern const xui_proxy_t tXgeProxy;
    xui_proxy_t tXgeProxy = xuiProxyXge();
xuiSetProxy(g_pCtx, &tXgeProxy);
    xuiSetViewportSize(g_pCtx, 640, 480);

    xui_widget pRoot;
xuiPanelCreate(g_pCtx, &pRoot, NULL);
    xuiWidgetSetRect(pRoot, (xui_rect_t){0, 0, 640, 480});

    /* HUD 按钮覆盖在游戏画面上 */
    xui_widget pBtn;
xuiButtonCreate(g_pCtx, &pBtn, NULL);
xuiWidgetAddChild(pRoot, pBtn);
    xuiLabelSetText(pBtn, "Pause");
    xuiWidgetSetRect(pBtn, (xui_rect_t){540, 20, 80, 36});
    xuiWidgetSetRect(pBtn, (xui_rect_t){0, 0, 80, 36});

    xgeRun(onFrame, NULL);
    xuiDestroy(g_pCtx);
    xgeUnit();
    return 0;
}

代码讲解

  • 渲染顺序:游戏先画(xgeClear + 场景),UI 后画覆盖,不可颠倒。
  • 输入路由:鼠标在 UI 控件上时不传递给游戏(HitTest 判定)。
  • 模态暂停:MessageBox 弹出时可暂停游戏逻辑,避免角色死亡。
  • 帧循环分阶段:Input → GameUpdate → GameRender → UIUpdate → UIRender → Present。

要点与陷阱

  • 渲染顺序:游戏先画,UI 后画覆盖,不可颠倒。
  • 输入路由:鼠标在 UI 控件上时不传递给游戏(HitTest 判定)。
  • 模态暂停:MessageBox 弹出时暂停游戏逻辑,避免角色死亡。
  • DPI:游戏坐标和 UI 坐标需统一 DPI 缩放。

进阶练习

  1. 暂停菜单:Esc 弹出暂停 UI,游戏逻辑冻结。
  2. 画中画:游戏渲染到 RenderTarget,UI 中 Image 显示。
应用骨架:场景 + UI 共存效果
图:应用骨架:场景 + UI 共存渲染效果