June 20th, 2018 (back)

RAPTR - Controller-Character Interaction

Progress Today

I had a blast designing the character-controller interface today. I decided to keep the paradigm of clear responsibilities going and it seemed to pay off well.

Controller Interface

The controller interface is rather lightweight. Functionally, it is a glorified dispatch table. I wanted classes to attach themselves and register callbacks with the controller and then let the event loop bubble events through the controller so they could be dispatched. This gave me the following design:

using ButtonCallback = std::function<bool(const Button& button)>;
using JoyCallback = std::function<bool(int32_t direction)>;

class Controller : public std::enable_shared_from_this<Controller> {
public:
   ...
  void on_right_joy(const JoyCallback& callback);
  void process_event(const SDL_Event& e);
  static std::shared_ptr<Controller> open(int32_t controller_id);

private:
  ...
  std::vector right_joy_callbacks;
};

Now, during initialization we register any found controllers rather simply.

bool Game::init_controllers()
{
  if (SDL_NumJoysticks() < 1) {
    std::cerr << "There are no controllers connected. What's the point of playing?\n";
    return false;
  }

  for (int32_t i = 0; i < SDL_NumJoysticks(); ++i) {
    if (!SDL_IsGameController(i)) {
      continue;
    }

    auto controller = Controller::open(i);
    controllers[controller->id()] = controller;
  }

  return !controllers.empty();
}

Character Interface

I wanted a flexible way to register/deregister controller input to characters, while still allowing me to do things like say... control multiple characters at once. The interface to the Character is then:

void Character::attach_controller(std::shared_ptr controller_)
{
  using namespace std::placeholders;

  controller = controller_;
  controller->on_right_joy(std::bind(&Character::on_right_joy, this, _1));
}

bool Character::on_right_joy(int32_t angle)
{
  if (angle < -4000) {
    this->walk_left();
  } else if (angle > 4000) {
    this->walk_right();
  } else {
    this->stop();
  }

  return true;
}

Where, once we have a character defined, we just attach a controller and let the class decide how it wants to handle inputs.

auto character = Character();
character.sprite = Sprite::from_json(...);
character.attach_controller(p1_controller);

Finally, our event loop is then augmented to dispatch on the Joystick DeviceID:

SDL_Event e;
if (SDL_PollEvent(&e)) {
  if (e.type == SDL_JOYBUTTONUP || e.type == SDL_JOYBUTTONDOWN ||
      e.type == SDL_JOYAXISMOTION || e.type == SDL_JOYHATMOTION) 
  {
    int32_t controller_id = e.jdevice.which;
    controllers[controller_id]->process_event(e);
  }
}

Raptr Party

I setup some basic instancing and caching on the renderer, then spawned 100 of these little guys all with the Controller attached.