在C语言中无法访问模块外部函数与变量时,如何应用单一职责原则(SRP)并重构事件驱动游戏代码?
解决C语言中单一职责原则(SRP)重构的跨模块访问问题
你遇到的问题其实是很多人第一次尝试SRP重构时都会踩的坑:输入模块不应该直接依赖主模块的内部状态和函数,这样不仅破坏了SRP,还会让模块间耦合得死死的。正确的思路是让输入模块只做「输入解析」这件事,把输入转换成标准化的动作信号,然后让主模块根据自己的状态来响应这些信号——这样既遵守了单一职责,又解决了跨模块访问的问题。
接下来我一步步给你拆解重构方案:
1. 先给输入动作做抽象定义
首先,我们要把用户的输入(比如按回车键、按空格)转换成不依赖游戏逻辑的抽象动作。在input.h里定义一个枚举来表示所有我们关心的输入动作:
// input.h #ifndef INPUT_H #define INPUT_H typedef enum { INPUT_QUIT, // 退出游戏 INPUT_PLACE_SHIP, // 放置船只 INPUT_TOGGLE_ACTION, // 对应空格:旋转或射击(主模块自己判断状态) INPUT_MOVE_UP, // 向上移动船只 INPUT_MOVE_DOWN, INPUT_MOVE_LEFT, INPUT_MOVE_RIGHT, INPUT_NONE // 无有效输入 } InputAction; // 处理输入,返回当前的输入动作 InputAction process_input(); #endif // INPUT_H
2. 实现输入模块:只做输入解析
输入模块的唯一工作就是捕获SDL事件,然后把它映射成我们定义的InputAction,完全不关心游戏里的任何状态或函数。这样它的职责就非常纯粹:
// input.c #include "input.h" #include <SDL2/SDL.h> InputAction process_input() { SDL_Event event; while (SDL_PollEvent(&event)) { switch (event.type) { case SDL_QUIT: return INPUT_QUIT; case SDL_KEYDOWN: switch (event.key.keysym.sym) { case SDLK_ESCAPE: return INPUT_QUIT; case SDLK_RETURN: return INPUT_PLACE_SHIP; case SDLK_SPACE: return INPUT_TOGGLE_ACTION; case SDLK_w: case SDLK_UP: return INPUT_MOVE_UP; // 在这里添加其他按键的映射 default: break; } default: break; } } return INPUT_NONE; }
3. 主模块负责响应输入动作
主模块拥有所有游戏状态(quit、isShooting、playerShips等)和逻辑函数,它只需要调用process_input()拿到输入动作,然后根据自己的状态来执行对应的逻辑——这样主模块也不用再处理繁琐的SDL事件解析了:
// main.c #include "input.h" #include <SDL2/SDL.h> // 定义游戏常量 #define WINDOW_WIDTH 800 #define WINDOW_HEIGHT 600 #define CELL_SIZE 32 #define GRID_OFFSET_Y 100 // 游戏状态和数据 typedef struct { SDL_Rect rect; // 其他船只属性,比如方向、长度等 } Ship; int quit = 0; int isShooting = 0; int placingShipIndex = 0; Ship playerShips[5]; Ship opponentShips[5]; SDL_Window* window; SDL_Renderer* renderer; // 游戏逻辑函数 void placeShip(Ship* ships) { // 你的原有放置船只逻辑 } void rotateShip(Ship* ships) { // 你的原有旋转船只逻辑 } void shootAtOpponent(SDL_Renderer* renderer, Ship* opponent, Ship* player) { // 你的原有射击逻辑 } void render() { // 你的原有渲染逻辑 SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255); SDL_RenderClear(renderer); // 渲染船只、网格等 SDL_RenderPresent(renderer); } int main() { // 初始化SDL if (SDL_Init(SDL_INIT_EVERYTHING) != 0) { // 错误处理 return 1; } SDL_CreateWindowAndRenderer(WINDOW_WIDTH, WINDOW_HEIGHT, SDL_WINDOW_RESIZABLE | SDL_WINDOW_MAXIMIZED, &window, &renderer); // 初始化游戏状态(比如船只位置) // ... // 主循环 while (!quit) { InputAction action = process_input(); // 根据输入动作响应 switch (action) { case INPUT_QUIT: quit = 1; break; case INPUT_PLACE_SHIP: if (!isShooting) { placeShip(playerShips); } break; case INPUT_TOGGLE_ACTION: if (!isShooting) { rotateShip(playerShips); } else { shootAtOpponent(renderer, opponentShips, playerShips); } break; case INPUT_MOVE_UP: if (!isShooting) { if (playerShips[placingShipIndex].rect.y != GRID_OFFSET_Y) { playerShips[placingShipIndex].rect.y -= CELL_SIZE; } } break; // 处理其他动作 case INPUT_NONE: default: break; } // 渲染帧 render(); } // 清理资源 SDL_DestroyRenderer(renderer); SDL_DestroyWindow(window); SDL_Quit(); return 0; }
为什么这符合SRP?
- 输入模块:只负责「捕获用户输入并转换成标准化动作」,完全独立于游戏逻辑,哪怕你把这个模块放到另一个SDL游戏里,只要修改枚举和按键映射就能复用。
- 主模块:负责游戏状态管理、逻辑响应和渲染,不用再处理繁琐的事件解析,职责更清晰。
进阶优化建议
如果你的游戏后续变得更复杂,还可以继续拆分模块:
- 把游戏状态(
quit、ships等)放到单独的game_state.h/c模块,提供初始化、修改状态的接口,主模块只调用这些接口,不直接操作状态变量。 - 把渲染逻辑拆到
render.h/c模块,主模块只调用render_game()这样的接口。 - 这样每个模块的职责都绝对单一,耦合度极低,后期维护和扩展会轻松很多。
内容的提问来源于stack exchange,提问作者Martin Zeltin
相关产品推荐
相关产品推荐

