gamepad support on pc and web

This commit is contained in:
ohsqueezy 2023-12-15 00:56:00 -05:00
parent 1db0a870d9
commit acb62ece0b
5 changed files with 171 additions and 8 deletions

View File

@ -96,7 +96,9 @@
"input":
{
"suppress any key on mods": true,
"any key ignore commands": ["left", "right", "up", "down", "pause"]
"any key ignore commands": ["left", "right", "up", "down", "pause"],
"gamepad pause button index": 9,
"gamepad axis cooldown": 0.2
},
"keys":
@ -281,6 +283,7 @@
"play texture": "resource/Play_Button.png",
"play translation": [0.0, 0.0],
"play scale": 0.6,
"play scale ratio": 1.0,
"name":
{
"arrow dimensions": [80.0, 40.0],
@ -410,7 +413,7 @@
"unlock buffalo": "✩ UNLOCKED BUFFALO BEEF CAKE ✩",
"unlock jackpot": "✩ UNLOCKED GOLDEN CAKE ✩",
"new best": "✩ NEW BEST TIME ✩",
"thanks": "Thank you playtesters @carlosissurreal @snakesandrews @paotato @big__flan @8ude @emilyrakoonce @xed @markkleeb @sortasoft @toddwords @computerlunch @synodai @sleepin @artfail @wondervillenyc @gumbo_nyc",
"thanks": " ",
"thanks translation": [0.3, -0.78],
"thanks scale": [1.58, 0.13],
"thanks wrap": 1080

2
lib/sb

@ -1 +1 @@
Subproject commit 8184a62a54feda11ea9543a7e615c4289ee0e593
Subproject commit 8e62f9e0a223deb5a48a67f401c6433257d6a936

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

After

Width:  |  Height:  |  Size: 108 KiB

View File

@ -104,12 +104,19 @@ Cakefoot::Cakefoot()
challenge_index = configuration()("progress", "current challenge");
view_index = configuration()("progress", "current view");
/* Subscribe to command events */
/* Subscribe to events */
delegate().subscribe(&Cakefoot::respond, this);
delegate().subscribe(&Cakefoot::respond, this, SDL_MOUSEMOTION);
delegate().subscribe(&Cakefoot::respond, this, SDL_MOUSEBUTTONDOWN);
delegate().subscribe(&Cakefoot::respond, this, SDL_MOUSEBUTTONUP);
delegate().subscribe(&Cakefoot::respond, this, SDL_MOUSEWHEEL);
delegate().subscribe(&Cakefoot::respond, this, SDL_JOYAXISMOTION);
delegate().subscribe(&Cakefoot::respond, this, SDL_JOYBUTTONDOWN);
delegate().subscribe(&Cakefoot::respond, this, SDL_JOYBUTTONUP);
delegate().subscribe(&Cakefoot::respond, this, SDL_KEYDOWN);
/* Open a game controller if any are available at the beginning of the program */
open_game_controller();
/* Set up playing field, the plane that provides the background of the curve, character, and enemies */
sb::Plane playing_field_plane;
@ -180,13 +187,59 @@ Cakefoot::Cakefoot()
#if defined(EMSCRIPTEN)
/* Pause the game when the browser tab is hidden */
if (emscripten_set_visibilitychange_callback(nullptr, false, &respond_to_visibility_change) < 0)
if (emscripten_set_visibilitychange_callback(this, false, &respond_to_visibility_change) < 0)
{
sb::Log::log("Failed to enable browser visibility change automatic pause feature", sb::Log::WARN);
}
/* Open the game controller when it is connected */
if (emscripten_set_gamepadconnected_callback(this, false, &respond_to_gamepad_connected) < 0)
{
sb::Log::log("Failed to listen for gamepad connections", sb::Log::WARN);
}
#endif
}
void Cakefoot::open_game_controller()
{
for (int ii = 0, jj = 0; ii < SDL_NumJoysticks(); ii++)
{
if (SDL_IsGameController(ii))
{
std::ostringstream message;
message << "Gamepad #" << ++jj << ": ";
std::string name {SDL_GameControllerNameForIndex(ii)};
if (name == "")
{
name = "[unnamed]";
}
message << name;
sb::Log::log(message);
if (controller.get() == nullptr)
{
controller = std::shared_ptr<SDL_GameController>(SDL_GameControllerOpen(ii), SDL_GameControllerClose);
std::ostringstream message;
if (controller.get() == nullptr)
{
message << "Could not open gamepad #" << jj;
sb::Log::sdl_error(message.str());
}
else
{
message << "Opened gamepad #" << jj;
sb::Log::log(message);
}
}
}
else
{
std::ostringstream message;
message << "Joystick #" << ii << " cannot be loaded by the game controller API";
sb::Log::log(message);
}
}
}
void Cakefoot::load_audio()
{
audio = {};
@ -611,7 +664,8 @@ void Cakefoot::set_up_buttons()
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
sb::Plane play_plane;
play_plane.texture(play_texture);
button.at("play") = sb::Pad<>{play_plane, configuration()("button", "play translation"), configuration()("button", "play scale"), 1.0f};
button.at("play") = sb::Pad<>{play_plane, configuration()("button", "play translation"), configuration()("button", "play scale"),
configuration()("button", "play scale ratio")};
button.at("play").state(original_state);
button.at("play").on_state_change([&](bool state){
load_level(0);
@ -1614,6 +1668,53 @@ void Cakefoot::refresh_scoreboard()
void Cakefoot::respond(SDL_Event& event)
{
/* Translate gamepad input to commands */
if (event.type == SDL_JOYBUTTONDOWN)
{
if (level_index > 0 && static_cast<std::size_t>(level_index) <= configuration()("levels").size() - 2 &&
event.jbutton.button == configuration()("input", "gamepad pause button index"))
{
sb::Delegate::post("pause");
}
else if (!use_play_button || button.at("play").pressed())
{
sb::Delegate::post("any");
}
}
else if (event.type == SDL_JOYBUTTONUP)
{
sb::Delegate::post("any", true);
}
else if (event.type == SDL_JOYAXISMOTION && !cooldown_animation.playing())
{
if (event.jaxis.axis == 1)
{
if (event.jaxis.value > 15000)
{
sb::Delegate::post("down");
cooldown_animation.play_once(configuration()("input", "gamepad axis cooldown"));
}
else if (event.jaxis.value < -15000)
{
sb::Delegate::post("up");
cooldown_animation.play_once(configuration()("input", "gamepad axis cooldown"));
}
}
else
{
if (event.jaxis.value > 15000)
{
sb::Delegate::post("right");
cooldown_animation.play_once(configuration()("input", "gamepad axis cooldown"));
}
else if (event.jaxis.value < -15000)
{
sb::Delegate::post("left");
cooldown_animation.play_once(configuration()("input", "gamepad axis cooldown"));
}
}
}
/* Get mouse button states */
bool left_mouse_pressed = SDL_GetMouseState(nullptr, nullptr) & SDL_BUTTON_LMASK;
bool shift_pressed = SDL_GetModState() & KMOD_SHIFT;
@ -2065,6 +2166,12 @@ void Cakefoot::respond(SDL_Event& event)
hovering = true;
}
}
/* Any keyboard input causes the play button to press */
else if (sb::Delegate::compare(event, "any"))
{
button.at("play").press();
}
}
/* Always collide with volume button and fullscreen if enabled */
@ -2096,6 +2203,11 @@ void Cakefoot::respond(SDL_Event& event)
}
}
bool Cakefoot::paused() const
{
return !unpaused_timer;
}
void Cakefoot::run()
{
/* Start timers precisely when the game update loop starts */
@ -2125,6 +2237,7 @@ void Cakefoot::update(float timestamp)
shift_hue_animation.update(timestamp);
flash_animation.update(timestamp);
blink_animation.update(timestamp);
cooldown_animation.update(timestamp);
/* Transformation for looking at the center of the field of play from the camera position. */
view = glm::lookAt(camera_position, {0.0f, 0.0f, 0.0f}, glm::vec3{0.0f, 1.0f, 0.0f});
@ -2664,15 +2777,29 @@ void Cakefoot::update(float timestamp)
sb::Log::gl_errors("at end of update");
}
void Cakefoot::quit()
{
controller.reset();
super::quit();
}
#if defined(EMSCRIPTEN)
EM_BOOL respond_to_visibility_change(int event_type, const EmscriptenVisibilityChangeEvent* visibility_change_event, void* user_data)
{
if (visibility_change_event->hidden)
Cakefoot* game = reinterpret_cast<Cakefoot*>(user_data);
if (visibility_change_event->hidden && !game->paused())
{
sb::Delegate::post("pause", false);
}
return true;
}
EM_BOOL respond_to_gamepad_connected(int event_type, const EmscriptenGamepadEvent* gamepad_event, void* user_data)
{
Cakefoot* game = reinterpret_cast<Cakefoot*>(user_data);
game->open_game_controller();
return true;
}
#endif
int main()

View File

@ -263,6 +263,7 @@ private:
sb::Color rotating_hue {128, 0, 0, 0};
std::vector<sb::Text> ending_messages;
std::optional<std::string> selected;
std::shared_ptr<SDL_GameController> controller = nullptr;
/*!
* Load sound effects and music into objects that can be used by the SDL mixer library. Use chunk objects for background music instead of
@ -451,6 +452,9 @@ private:
/* Toggle visibility flag every interval */
Animation blink_animation {std::bind(&Cakefoot::blink, this), configuration()("display", "blink frequency")};
/* Count a cooldown period for gamepad axes */
Animation cooldown_animation;
/*!
* Get the arcade time as the amount of time remaining before the limit is reached.
*
@ -479,11 +483,21 @@ public:
*/
Cakefoot();
/*!
* Log detected joysticks and open the first one that is usable as a game controller
*/
void open_game_controller();
/*!
* Respond to command events
*/
void respond(SDL_Event&);
/*!
* @return true if the game is currently paused
*/
bool paused() const;
/*!
* Start timers, enable auto refresh, and run the super class's run function, which starts the game's update loop.
*
@ -498,6 +512,10 @@ public:
*/
void update(float timestamp);
/*!
* Close the controller, then call Game::quit()
*/
void quit();
};
#if defined(EMSCRIPTEN)
@ -508,10 +526,25 @@ public:
*
* @param event_type Emscripten's ID for the visibilitychange event
* @param visibility_change_event Emscripten's event object
* @param user_data Emscripten's API allows wser data to be passed, but this is treated as null
* @param user_data The game object passed as a void pointer through Emscripten's API
* @return True to indicate that the event was consumed by the handler
*/
EM_BOOL respond_to_visibility_change(int event_type, const EmscriptenVisibilityChangeEvent* visibility_change_event, void* user_data);
/*!
* This event handler should be registered with emscripten_set_gamepadconnected_callback so it will be called automatically when a
* gamepad is connected. If there isn't a gamepad already registered, the first detected gamepad will be registered as the game's
* controller.
*
* Even if a USB gamepad is connected when the program loads, the web browser may not register it as connected until a button is
* pressed, so this event will need to be triggered before any controller is usable on a web page.
*
* @param event_type Emscripten's ID for the gamepadconnected event
* @param visibility_change_event Emscripten's event object
* @param user_data The game object passed as a void pointer through Emscripten's API
* @return True to indicate that the event was consumed by the handler
*/
EM_BOOL respond_to_gamepad_connected(int event_type, const EmscriptenGamepadEvent* gamepad_event, void* user_data);
#endif
/*!