Chapter 20 · 渲染管线

场景栈管理

掌握 XGE 的场景(Scene)栈机制,实现菜单→游戏→暂停等多界面切换,理解生命周期回调。

20

概念:场景栈

游戏通常有多个"界面":主菜单、游戏关卡、暂停菜单、设置页面。XGE 用场景栈管理这些界面——栈顶是当前活跃场景,只有栈顶场景接收 Update 和 Draw。

每个场景是一个 xge_scene_t 结构体,包含生命周期回调(进入/离开/暂停/恢复)和每帧回调(事件/更新/绘制)。

xge_scene_t 结构体

struct xge_scene_t {
    void* pUser;                    // 用户数据(场景私有状态)
    xge_scene_lifecycle_proc onEnter;   // 进入场景
    xge_scene_lifecycle_proc onLeave;   // 离开场景
    xge_scene_lifecycle_proc onPause;   // 被压栈(暂停)
    xge_scene_lifecycle_proc onResume;  // 从栈中恢复
    xge_scene_event_proc     onEvent;  // 事件处理
    xge_scene_update_proc    onUpdate; // 逻辑更新 (fDelta)
    xge_scene_draw_proc      onDraw;   // 绘制
    xge_scene_lifecycle_proc onFree;   // 释放资源
    int bEntered;                     // 是否已进入
    int bPaused;                      // 是否暂停
};

API 速查

函数说明
int xgeSceneSet(xge_scene pScene)替换整个栈,设为唯一场景
int xgeScenePush(xge_scene pScene)压入新场景(当前场景暂停)
int xgeScenePop(void)弹出栈顶(恢复下一层)
int xgeSceneReplace(xge_scene pScene)替换栈顶(不改变栈深度)
xge_scene xgeSceneCurrent(void)获取栈顶场景
int xgeSceneCount(void)获取栈中场景数量
int xgeSceneDispatchEvent(const xge_event_t* pEvent)向栈顶场景分发事件
int xgeSceneUpdateStrategySet(int iMode, float fFixedStep, int iMaxUpdates)设置更新策略
void xgeSceneUpdateStrategyGet(int* pMode, float* pFixedStep, int* pMaxUpdates)获取更新策略

编译本章示例

gcc -O2 -I. -o ch20.exe ch20_main.c build\xge.lib -lm -lws2_32 -liphlpapi -lgdi32 -luser32 -lshell32 -lopengl32 -lole32 -lwinmm -lavrt
.\ch20.exe

示例 1:定义场景

static int menuEnter(xge_scene s) {
    xgeLogWrite(XGE_LOG_INFO, "Menu entered");
    return XGE_OK;
}

static int menuUpdate(xge_scene s, float fDelta) {
    // 检测按键 → 进入游戏
    if (xgeKeyPressed(XGE_KEY_ENTER)) {
        xgeScenePush(&g_tGameScene);  // 压入游戏场景
    }
    return XGE_OK;
}

static int menuDraw(xge_scene s) {
    xgeClear(0x081114FF);
    // xgeTextDraw(300, 250, "PRESS ENTER TO START", ...);
    return XGE_OK;
}

static xge_scene_t g_tMenuScene = {
    .onEnter  = menuEnter,
    .onUpdate = menuUpdate,
    .onDraw   = menuDraw,
};

示例 2:场景切换流程

int main(void) {
    xge_desc_t d = {0};
    d.iWidth = 800; d.iHeight = 600;
    xgeInit(&d);

    // 设置初始场景(清空栈,压入菜单)
    xgeSceneSet(&g_tMenuScene);

    // 引擎自动调用栈顶场景的 onUpdate / onDraw
    xgeRun(NULL, NULL);  // 场景模式下回调可为 NULL
    xgeUnit();
    return 0;
}

示例 3:暂停菜单(Push/Pop)

// 游戏中按 ESC → 压入暂停场景
static int gameUpdate(xge_scene s, float fDelta) {
    if (xgeKeyPressed(XGE_KEY_ESCAPE)) {
        xgeScenePush(&g_tPauseScene);
        // 游戏场景自动收到 onPause
    }
    // ... 游戏逻辑 ...
    return XGE_OK;
}

// 暂停场景中按 Resume → 弹出
static int pauseUpdate(xge_scene s, float fDelta) {
    if (xgeKeyPressed(XGE_KEY_ESCAPE)) {
        xgeScenePop();
        // 游戏场景自动收到 onResume
    }
    return XGE_OK;
}

生命周期时序

// Push 新场景:
//   旧栈顶.onPause() → 新场景.onEnter()
//
// Pop 栈顶:
//   栈顶.onLeave() → 栈顶.onFree() → 新栈顶.onResume()
//
// Replace 栈顶:
//   旧栈顶.onLeave() → 旧栈顶.onFree() → 新场景.onEnter()
//
// Set(清栈):
//   所有场景依次 onLeave + onFree → 新场景.onEnter()

完整代码

将以下代码保存为 ch20_main.c,编译后运行即可体验场景栈切换(Enter 进入游戏,ESC 返回菜单):

#include "xge.h"

static xge_scene_t g_tMenuScene;
static xge_scene_t g_tGameScene;

/* --- 菜单场景 --- */
static int menuUpdate(xge_scene s, float fDelta) {
    (void)s; (void)fDelta;
    if (xgeKeyPressed(XGE_KEY_ENTER))
        xgeScenePush(&g_tGameScene);
    return XGE_OK;
}
static int menuDraw(xge_scene s) {
    (void)s;
    xgeClear(0x081114FF);
    xgeShapeRectFill(250, 250, 300, 100, 0x1A2A30FF);
    xgeShapeCircleFill(400, 300, 30, 0x4FD8C2FF);
    return XGE_OK;
}

/* --- 游戏场景 --- */
static int gameUpdate(xge_scene s, float fDelta) {
    (void)s; (void)fDelta;
    if (xgeKeyPressed(XGE_KEY_ESCAPE))
        xgeScenePop();
    return XGE_OK;
}
static int gameDraw(xge_scene s) {
    (void)s;
    xgeClear(0x14100AFF);
    xgeShapeCircleFill(400, 300, 60, 0xFFB454FF);
    return XGE_OK;
}

int main(void) {
    // 初始化场景结构体
    g_tMenuScene.onUpdate = menuUpdate;
    g_tMenuScene.onDraw   = menuDraw;
    g_tGameScene.onUpdate = gameUpdate;
    g_tGameScene.onDraw   = gameDraw;

    xge_desc_t d = {0};
    d.iWidth  = 800;
    d.iHeight = 600;
    d.sTitle  = "ch20 - Scene Stack";
    d.iFlags  = XGE_INIT_VSYNC;
    xgeInit(&d);

    xgeSceneSet(&g_tMenuScene);  // 设置初始场景
    xgeRun(NULL, NULL);           // 场景模式下回调可为 NULL
    xgeUnit();
    return 0;
}

代码讲解

  • xgeSceneSet(&g_tMenuScene) 清空场景栈并压入菜单场景作为初始界面。
  • xgeScenePush 压入游戏场景时,菜单场景自动收到 onPause。
  • xgeScenePop 弹出游戏场景后,菜单场景自动收到 onResume 并重新活跃。
  • xgeRun(NULL, NULL):场景模式下引擎自动调用栈顶场景的 onUpdate/onDraw,无需额外帧回调。

要点与陷阱

  • 场景是结构体,不是指针:xge_scene 是指针类型,指向你的 xge_scene_t 实例。通常用全局/静态变量。
  • onFree 释放资源:Pop/Replace 后引擎调用 onFree。在此释放 malloc 的内存、纹理等。
  • 只有栈顶活跃:被压住的场景不会收到 onUpdate/onDraw/onEvent。
  • pUser 存私有数据:每个场景通过 pUser 指向自己的状态结构,避免全局变量污染。
  • Update 策略:xgeSceneUpdateStrategySet 可设置固定步长更新(适合物理模拟)。
  • 与 xgeRun 回调共存:如果同时设置了 xgeRun 的帧回调和场景,两者都会被调用。

进阶练习

  1. 实现三层场景栈:主菜单 → 关卡选择 → 游戏,ESC 逐层返回。
  2. 用 pUser 为每个场景存储独立状态(如关卡编号、玩家分数)。
  3. 实现场景切换动画:onEnter 中渐入(Alpha 0→1),onLeave 中渐出。
场景栈管理效果
图:场景栈管理渲染效果