使用SDL库读取手柄摇杆数据

由于之前使用 MMSystem 库对手柄的数据进行读取 [[2023-10-17]] 的方式,在笔记本上接手柄总是出现一些虚拟手柄占用接口的问题(未找到原因)。另外找一种读取手柄数据的方式。
简单介绍一下使用 [[SDL]] 库读取手柄摇杆的方法。主要参考的源码是[1]

初始化手柄子系统

bool init() {
  // Initialization flag
  bool success = true;

  // Initialize SDL
  if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_JOYSTICK) < 0) {  // 源码里为了可视化手柄数值,进行了箭头绘制,所以额外初始化了一个video子系统
    printf("SDL could not initialize! SDL Error: %s\n", SDL_GetError());
    success = false;
  } else {
    // Set texture filtering to linear
    if (!SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "1")) {
      printf("Warning: Linear texture filtering not enabled!");
    }

    // Check for joysticks
    if (SDL_NumJoysticks() < 1) {
      printf("Warning: No joysticks connected!\n");
    } else {
      // Load joystick
      gGameController = SDL_JoystickOpen(0);
      if (gGameController == NULL) {
        printf("Warning: Unable to open game controller! SDL Error: %s\n",
               SDL_GetError());
      }
    }
  }

  return success;
}

手柄关闭子系统

void close() {
  // Close game controller
  SDL_JoystickClose(gGameController);
  gGameController = NULL;

  // Quit SDL subsystems
  SDL_Quit();
}

主程序

int WinMain(int argc, char* args[]) {
  // Start up SDL and create window
  if (!init()) {
    printf("Failed to initialize!\n");
  } else {
    bool quit = false;

    // Event handler
    SDL_Event e;

    // Normalized direction
    int xDir = 0;
    int yDir = 0;

    // While application is running
    while (!quit) {
      // Handle events on queue
      while (SDL_PollEvent(&e) != 0) {
        // User requests quit
        if (e.type == SDL_QUIT) {
          quit = true;
        } else if (e.type == SDL_JOYAXISMOTION) {
          // Motion on controller 0
          if (e.jaxis.which == 0) {
            std::cout<<"e.jaxis.axis: "<<e.jaxis.axis<<std::endl;
            // X axis motion
            if (e.jaxis.axis == 0) {
              // Left of dead zone
              if (e.jaxis.value < -JOYSTICK_DEAD_ZONE) {
                xDir = e.jaxis.value;
              }
              // Right of dead zone
              else if (e.jaxis.value > JOYSTICK_DEAD_ZONE) {
                xDir = e.jaxis.value;
              } else {
                xDir = 0;
              }
            }
            // Y axis motion
            else if (e.jaxis.axis == 1) {
              // Below of dead zone
              if (e.jaxis.value < -JOYSTICK_DEAD_ZONE) {
                yDir = e.jaxis.value;
              }
              // Above of dead zone
              else if (e.jaxis.value > JOYSTICK_DEAD_ZONE) {
                yDir = e.jaxis.value;
              } else {
                yDir = 0;
              }
            }
          }
        }
      }

      // Calculate angle
      double joystickAngle = atan2((double)yDir, (double)xDir) * (180.0 / M_PI);
      std::cout << "yDire: " << yDir << " xDir: " << xDir << std::endl;
      // Correct angle
      if (xDir == 0 && yDir == 0) {
        joystickAngle = 0;
      }
    }
  }

  // Free resources and close SDL
  close();

  return 0;
}

关键的逻辑是,通过一个事件 SDL_Event 来表示摇杆是否运动,如果摇杆发生了运动,通过 axis 这个属性判断是 x 还是 y 方向的运动,然后保存到相应的摇杆位置状态值中。

识别手柄被拔出

同样通过事件类型判断手柄是否被拔出

else if (e.type == SDL_JOYDEVICEREMOVED) {
          printf("Warning: A joystick is removed!\n");
          if (SDL_NumJoysticks() < 1) {
            SDL_JoystickClose(gGameController);
            gGameController = NULL;
            xDir = 0;
            yDir = 0;
            printf("Warning: No joysticks connected!\n");
          }
          plugtimes++;

这里只是对单手柄的一个实现,多个手柄的逻辑会比较复杂。
特别要说明的是,plugtimes 虽然字面意思是描述插拔了几次,实际上和前面代码中的 e.jaxis.which 有关,每次插拔都会识别成一个新的手柄,所以 e.jaxis.which 这个索引量就会加 1.

识别手柄插入

同样通过事件类型判断手柄是否插入

else if (e.type == SDL_JOYDEVICEADDED) {
          if (SDL_NumJoysticks() == 1) {
            gGameController = SDL_JoystickOpen(0);
            if (gGameController == NULL) {
              printf("Warning: Unable to open game controller! SDL Error: %s\n",
                     SDL_GetError());
            }
          }
        }

SDL无法正常捕获事件的问题

造成 [[SDL]] 的函数无法正常捕获所有的操作,事件类型是 SDL_POLLSENTINEL = 0x7F00
查看文档发现需要初始化 SDL_INIT_VIDEO 子系统[2]
正确的启动方式是

if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_JOYSTICK) < 0)

另外一个原因,必须由主线程捕获事件。[3] [4] [5]或者说只有主线程才能捕获到事件。

通过上述两处修改解决。

Reference


  1. Lazy Foo' Productions - Gamepads and Joysticks ↩︎

  2. Events - SDL Library Documentation ↩︎

  3. Event thread - SDL Development - Simple Directmedia Layer (libsdl.org) ↩︎

  4. Multi-threaded Programming - SDL Library Documentation ↩︎

  5. Threads and events - SDL Development - Simple Directmedia Layer (libsdl.org) ↩︎

posted @ 2024-02-04 20:30  pomolnc  阅读(182)  评论(0编辑  收藏  举报