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
相关产品推荐
相关产品推荐

