You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

C语言基于SDL2实现游戏菜单/场景的优质方案是什么

SDL2 单全局循环菜单架构实现方案

直接采用抽象状态接口+状态栈的标准游戏状态架构,完全替代嵌套循环、全局变量判断菜单的旧写法,三个需求可全部覆盖,后期扩展新菜单成本极低。


核心设计逻辑

把每个菜单、游戏运行场景都抽象为独立的「状态」,所有状态遵循统一的接口规范;全局仅维护一个状态栈,主循环永远只和栈顶的激活状态交互,不需要写大量分支判断当前所处菜单。

第一步:定义通用状态接口

所有菜单必须实现4个固定方法,对应三个核心需求:

// 统一状态结构体,每个菜单/游戏场景都按这个模板实现
typedef struct GameState {
    // 单次初始化:状态首次激活时仅执行1次,满足单次初始化需求
    void (*init)(struct GameState* self);
    // 事件处理:当前状态独立实现自己的输入响应逻辑,满足差异化事件处理需求
    void (*handle_event)(struct GameState* self, SDL_Event* ev);
    // 帧更新:每帧执行当前状态的业务逻辑
    void (*update)(struct GameState* self, float delta_time);
    // 资源销毁:状态退出时执行,释放所有该状态申请的内存/资源,满足生命周期内存管理需求
    void (*destroy)(struct GameState* self);
    // 私有数据指针:每个状态自己malloc的变量、加载的纹理/字体等资源全存在这里,不需要全局变量
    void* instance_data;
} GameState;

第二步:实现全局状态栈管理器

全局只需要一个栈结构管理所有激活的状态,压栈=进入新菜单,弹栈=退出当前菜单回到上一级:

#define MAX_STACK_DEPTH 16
typedef struct StateManager {
    GameState* stack[MAX_STACK_DEPTH];
    int top;
} StateManager;

// 压入新状态(进入新菜单/场景)
void state_push(StateManager* mgr, GameState* new_state) {
    new_state->init(new_state); // 压入瞬间执行单次初始化,全程只跑这一次
    mgr->stack[mgr->top++] = new_state;
}

// 弹出栈顶状态(退出当前菜单/场景)
void state_pop(StateManager* mgr) {
    if (mgr->top <= 0) return;
    GameState* current = mgr->stack[--mgr->top];
    current->destroy(current); // 弹出瞬间执行资源释放,所有该状态申请的内存全在这里free
    free(current);
}

第三步:写全局唯一主循环

整个游戏就保留这一个while循环,彻底删掉之前每个菜单独立写的嵌套循环:

int main(int argc, char* argv[]) {
    // 常规SDL2初始化
    SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS);
    SDL_Window* win = SDL_CreateWindow("Game", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 1280, 720, 0);
    SDL_Renderer* renderer = SDL_CreateRenderer(win, -1, SDL_RENDERER_ACCELERATED);

    StateManager mgr = {.top = 0};
    // 启动时压入初始主菜单
    state_push(&mgr, create_main_menu(renderer));

    int running = 1;
    Uint64 last_tick = SDL_GetPerformanceCounter();
    while (running) {
        // 计算帧间隔
        Uint64 now = SDL_GetPerformanceCounter();
        float dt = (now - last_tick) * 1.0f / SDL_GetPerformanceFrequency();
        last_tick = now;

        SDL_Event ev;
        while (SDL_PollEvent(&ev)) {
            if (ev.type == SDL_QUIT) {
                running = 0;
                break;
            }
            // 事件只发给当前栈顶的激活状态处理
            if (mgr.top > 0) {
                mgr.stack[mgr.top - 1]->handle_event(mgr.stack[mgr.top - 1], &ev);
            }
        }
        if (!running) break;

        // 只更新栈顶激活状态的逻辑
        if (mgr.top > 0) {
            mgr.stack[mgr.top - 1]->update(mgr.stack[mgr.top - 1], dt);
        }

        // 渲染逻辑同理,给状态加个render方法即可
        SDL_RenderClear(renderer);
        // mgr.stack[mgr.top-1]->render(...)
        SDL_RenderPresent(renderer);
    }

    // 退出时依次弹出栈内所有状态,全部释放资源,无遗漏
    while (mgr.top > 0) {
        state_pop(&mgr);
    }

    SDL_DestroyRenderer(renderer);
    SDL_DestroyWindow(win);
    SDL_Quit();
    return 0;
}

具体菜单实现示例(主菜单)

每个菜单独立写自己的四个接口方法即可,和其他菜单完全解耦:

// 主菜单私有数据,所有主菜单动态申请的资源都存在这里
typedef struct MainMenuPriv {
    TTF_Font* font;
    SDL_Texture* bg;
    int select_idx;
} MainMenuPriv;

// 主菜单初始化:只在压栈时执行一次
void main_menu_init(GameState* self) {
    MainMenuPriv* priv = malloc(sizeof(MainMenuPriv));
    priv->select_idx = 0;
    priv->font = TTF_OpenFont("res/font.ttf", 36);
    priv->bg = IMG_LoadTexture(get_renderer(), "res/menu_bg.png");
    self->instance_data = priv;
}

// 主菜单事件处理:只响应上下选择、回车确认,不处理玩家移动等无关按键
void main_menu_handle_event(GameState* self, SDL_Event* ev) {
    MainMenuPriv* priv = (MainMenuPriv*)self->instance_data;
    StateManager* mgr = get_global_mgr();
    if (ev->type == SDL_KEYDOWN) {
        switch (ev->key.keysym.sym) {
            case SDLK_UP:
                priv->select_idx = priv->select_idx > 0 ? priv->select_idx -1 : 2;
                break;
            case SDLK_DOWN:
                priv->select_idx = (priv->select_idx +1) %3;
                break;
            case SDLK_RETURN:
                if (priv->select_idx == 0) {
                    // 选开始游戏:弹出主菜单,压入游戏场景
                    state_pop(mgr);
                    state_push(mgr, create_gameplay_state());
                }
                // 其他选项逻辑省略
                break;
        }
    }
}

// 主菜单帧更新:处理按钮动画、光标闪烁等逻辑
void main_menu_update(GameState* self, float dt) {
    // 逻辑省略
}

// 主菜单资源释放:所有申请的内存、加载的资源全部在这里释放
void main_menu_destroy(GameState* self) {
    MainMenuPriv* priv = (MainMenuPriv*)self->instance_data;
    TTF_CloseFont(priv->font);
    SDL_DestroyTexture(priv->bg);
    free(priv);
}

// 主菜单构造函数,返回填充好接口的GameState结构体
GameState* create_main_menu(SDL_Renderer* r) {
    GameState* s = malloc(sizeof(GameState));
    s->init = main_menu_init;
    s->handle_event = main_menu_handle_event;
    s->update = main_menu_update;
    s->destroy = main_menu_destroy;
    s->instance_data = NULL;
    return s;
}

需求匹配说明

  • 差异化事件处理:每个状态独立实现输入逻辑,主菜单只响应菜单按键,游戏场景只响应移动/交互按键,设置菜单只响应配置调整操作,完全不需要写全局分支判断当前菜单类型,逻辑不会串
  • 生命周期内存管理:每个状态所有动态申请的内存、加载的资源全部存在自己的私有数据区,状态弹出时destroy方法会被强制调用,只要在destroy里对应释放所有申请的资源,完全不会出现内存泄漏,游戏退出时栈内剩余状态会被依次弹出释放,没有遗漏
  • 单次初始化机制:状态仅在压入栈时执行一次init,只要不被弹出重新压入,就不会重复执行初始化流程,不会重复加载资源

如果需要实现暂停菜单这类半透明覆盖层(弹出暂停时能看到底下的游戏画面),只需要给GameState加个is_overlay的标记,渲染时从栈底第一个非覆盖层状态开始往上渲染即可,扩展成本极低。

内容的提问来源于stack exchange,提问作者Lorenzo Vitali

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 14:48:18