From 9e541deba7c39097e47b6beee8e9ba3afb834667 Mon Sep 17 00:00:00 2001 From: Allan Legemaate Date: Sat, 14 Mar 2026 01:20:03 -0400 Subject: [PATCH 1/8] feat: add action bindings --- include/asw/asw.h | 1 + include/asw/modules/action.h | 122 +++++++++++++++++++++ src/modules/action.cpp | 202 +++++++++++++++++++++++++++++++++++ src/modules/input.cpp | 5 + 4 files changed, 330 insertions(+) create mode 100644 include/asw/modules/action.h create mode 100644 src/modules/action.cpp diff --git a/include/asw/asw.h b/include/asw/asw.h index 29f1458..558ba3b 100644 --- a/include/asw/asw.h +++ b/include/asw/asw.h @@ -3,6 +3,7 @@ #define SDL_MAIN_HANDLED +#include "./modules/action.h" #include "./modules/assets.h" #include "./modules/color.h" #include "./modules/core.h" diff --git a/include/asw/modules/action.h b/include/asw/modules/action.h new file mode 100644 index 0000000..7897e5b --- /dev/null +++ b/include/asw/modules/action.h @@ -0,0 +1,122 @@ +/// @file action.h +/// @author Allan Legemaate (alegemaate@gmail.com) +/// @brief Action binding system for the ASW library +/// @date 2026-03-14 +/// +/// @copyright Copyright (c) 2026 +/// +/// Allows registering named actions backed by one or more mouse, keyboard or +/// controller bindings. Any binding that is active will satisfy the action. +/// +/// Example: +/// @code +/// asw::input::bind_action("jump", asw::input::KeyBinding{asw::input::Key::Space}); +/// asw::input::bind_action("jump", asw::input::ControllerButtonBinding{asw::input::ControllerButton::A}); +/// +/// // In game loop: +/// if (asw::input::is_action_pressed("jump")) { /* ... */ } +/// @endcode + +#ifndef ASW_ACTION_H +#define ASW_ACTION_H + +#include +#include +#include + +#include "./input.h" + +namespace asw::input { + +/// @brief Binding to a keyboard key. +struct KeyBinding { + Key key; +}; + +/// @brief Binding to a mouse button. +struct MouseButtonBinding { + MouseButton button; +}; + +/// @brief Binding to a controller (gamepad) button. +struct ControllerButtonBinding { + ControllerButton button; + uint32_t controller_index { 0 }; +}; + +/// @brief Binding to a controller axis, activated when the axis exceeds a threshold. +/// +/// @details Set @c positive_direction to false to bind to the negative axis direction +/// (e.g. left stick left, or left trigger in inverted mode). +struct ControllerAxisBinding { + ControllerAxis axis; + uint32_t controller_index { 0 }; + float threshold { 0.5F }; + bool positive_direction { true }; +}; + +/// @brief A single input binding — keyboard, mouse button, controller button, or controller axis. +using ActionBinding = std::variant; + +/// @brief Register a binding for a named action. +/// +/// Multiple bindings can be added to the same action; any active binding will +/// satisfy the action (logical OR). +/// +/// @param name The action name. +/// @param binding The input binding to associate with the action. +/// +void bind_action(std::string_view name, ActionBinding binding); + +/// @brief Remove all bindings for a named action. +/// +/// @param name The action name to remove. +/// +void unbind_action(std::string_view name); + +/// @brief Remove all registered actions and their bindings. +/// +void clear_actions(); + +/// @brief Check if an action was triggered (first pressed) this frame. +/// +/// @param name The action name. +/// @return true if any binding transitioned to active this frame. +/// +bool is_action_pressed(std::string_view name); + +/// @brief Check if an action was released this frame. +/// +/// @param name The action name. +/// @return true if any binding transitioned to inactive this frame. +/// +bool is_action_released(std::string_view name); + +/// @brief Check if an action is currently held down. +/// +/// @param name The action name. +/// @return true if any binding is currently active. +/// +bool is_action_down(std::string_view name); + +/// @brief Get the analogue strength of an action (0.0 – 1.0). +/// +/// For button/key bindings this is 0 or 1. For axis bindings it is the +/// normalised axis value clamped to the range [threshold, 1.0]. +/// Returns the maximum strength across all active bindings. +/// +/// @param name The action name. +/// @return float Strength in [0.0, 1.0]. +/// +float get_action_strength(std::string_view name); + +/// @brief Update cached action states from current raw input. +/// +/// Called automatically by asw::input::reset() — you do not need to call +/// this yourself unless you are managing the input loop manually. +/// +void update_actions(); + +} // namespace asw::input + +#endif // ASW_ACTION_H diff --git a/src/modules/action.cpp b/src/modules/action.cpp new file mode 100644 index 0000000..ecdcdbc --- /dev/null +++ b/src/modules/action.cpp @@ -0,0 +1,202 @@ +#include "./asw/modules/action.h" + +#include +#include +#include + +namespace { + +struct ActionData { + std::vector bindings; + + bool pressed { false }; + bool released { false }; + bool down { false }; + float strength { 0.0F }; + + /// Tracks the down state from the previous frame to derive axis transitions. + bool prev_down { false }; +}; + +std::unordered_map action_map; + +// --------------------------------------------------------------------------- +// Per-binding query helpers +// --------------------------------------------------------------------------- + +/// Returns true if the binding is currently active, and updates out_strength. +bool binding_is_down(const asw::input::ActionBinding& binding, float& out_strength) +{ + return std::visit( + [&out_strength](const auto& b) -> bool { + using T = std::decay_t; + + if constexpr (std::is_same_v) { + if (asw::input::keyboard.down[static_cast(b.key)]) { + out_strength = std::max(out_strength, 1.0F); + return true; + } + return false; + + } else if constexpr (std::is_same_v) { + if (asw::input::mouse.down[static_cast(b.button)]) { + out_strength = std::max(out_strength, 1.0F); + return true; + } + return false; + + } else if constexpr (std::is_same_v) { + if (b.controller_index < asw::input::controller.size() + && asw::input::controller[b.controller_index] + .down[static_cast(b.button)]) { + out_strength = std::max(out_strength, 1.0F); + return true; + } + return false; + + } else if constexpr (std::is_same_v) { + if (b.controller_index < asw::input::controller.size()) { + const float val + = asw::input::controller[b.controller_index].axis[static_cast(b.axis)]; + const float effective = b.positive_direction ? val : -val; + if (effective >= b.threshold) { + out_strength = std::max(out_strength, effective); + return true; + } + } + return false; + } + + return false; + }, + binding); +} + +/// Returns true if the binding was pressed this frame (digital sources only). +/// Axis bindings return false here — their transitions are handled via prev_down. +bool binding_is_pressed(const asw::input::ActionBinding& binding) +{ + return std::visit( + [](const auto& b) -> bool { + using T = std::decay_t; + + if constexpr (std::is_same_v) { + return asw::input::keyboard.pressed[static_cast(b.key)]; + + } else if constexpr (std::is_same_v) { + return asw::input::mouse.pressed[static_cast(b.button)]; + + } else if constexpr (std::is_same_v) { + return b.controller_index < asw::input::controller.size() + && asw::input::controller[b.controller_index] + .pressed[static_cast(b.button)]; + + } else { + // ControllerAxisBinding: transition handled by prev_down in update_actions. + return false; + } + }, + binding); +} + +/// Returns true if the binding was released this frame (digital sources only). +bool binding_is_released(const asw::input::ActionBinding& binding) +{ + return std::visit( + [](const auto& b) -> bool { + using T = std::decay_t; + + if constexpr (std::is_same_v) { + return asw::input::keyboard.released[static_cast(b.key)]; + + } else if constexpr (std::is_same_v) { + return asw::input::mouse.released[static_cast(b.button)]; + + } else if constexpr (std::is_same_v) { + return b.controller_index < asw::input::controller.size() + && asw::input::controller[b.controller_index] + .released[static_cast(b.button)]; + + } else { + // ControllerAxisBinding: transition handled by prev_down in update_actions. + return false; + } + }, + binding); +} + +} // namespace + +// --------------------------------------------------------------------------- +// Public API +// --------------------------------------------------------------------------- + +void asw::input::bind_action(std::string_view name, asw::input::ActionBinding binding) +{ + action_map[std::string(name)].bindings.push_back(binding); +} + +void asw::input::unbind_action(std::string_view name) +{ + action_map.erase(std::string(name)); +} + +void asw::input::clear_actions() +{ + action_map.clear(); +} + +bool asw::input::is_action_pressed(std::string_view name) +{ + auto it = action_map.find(std::string(name)); + return it != action_map.end() && it->second.pressed; +} + +bool asw::input::is_action_released(std::string_view name) +{ + auto it = action_map.find(std::string(name)); + return it != action_map.end() && it->second.released; +} + +bool asw::input::is_action_down(std::string_view name) +{ + auto it = action_map.find(std::string(name)); + return it != action_map.end() && it->second.down; +} + +float asw::input::get_action_strength(std::string_view name) +{ + auto it = action_map.find(std::string(name)); + return it != action_map.end() ? it->second.strength : 0.0F; +} + +void asw::input::update_actions() +{ + for (auto& [name, action] : action_map) { + bool any_down = false; + bool any_pressed = false; + bool any_released = false; + float max_strength = 0.0F; + + for (const auto& binding : action.bindings) { + any_down |= binding_is_down(binding, max_strength); + any_pressed |= binding_is_pressed(binding); + any_released |= binding_is_released(binding); + } + + // Derive press / release transitions for axis bindings (and as a + // fallback for any binding that doesn't supply its own signals). + if (any_down && !action.prev_down) { + any_pressed = true; + } + if (!any_down && action.prev_down) { + any_released = true; + } + + action.pressed = any_pressed; + action.released = any_released; + action.down = any_down; + action.strength = max_strength; + action.prev_down = any_down; + } +} diff --git a/src/modules/input.cpp b/src/modules/input.cpp index edceb22..b89edc9 100644 --- a/src/modules/input.cpp +++ b/src/modules/input.cpp @@ -1,5 +1,7 @@ #include "./asw/modules/input.h" +#include "./asw/modules/action.h" + namespace { /// @brief Active cursor stores the current active cursor. It is updated by /// the core. @@ -16,6 +18,9 @@ std::string asw::input::text_input; void asw::input::reset() { + // Snapshot action states before raw input arrays are cleared. + asw::input::update_actions(); + auto& k_state = asw::input::keyboard; auto& m_state = asw::input::mouse; auto& c_state = asw::input::controller; From 2853c2cfce29c0416d7cb49f49b64af66a2ab23b Mon Sep 17 00:00:00 2001 From: Allan Legemaate Date: Sat, 14 Mar 2026 11:07:15 -0400 Subject: [PATCH 2/8] feat: add examples --- .gitignore | 1 + CMakeLists.txt | 6 ++ Doxyfile | 2 +- README.md | 11 +++ examples/CMakeLists.txt | 7 ++ examples/actions/CMakeLists.txt | 2 + examples/actions/main.cpp | 146 ++++++++++++++++++++++++++++ examples/controller/CMakeLists.txt | 2 + examples/controller/main.cpp | 147 +++++++++++++++++++++++++++++ examples/keyboard/CMakeLists.txt | 2 + examples/keyboard/main.cpp | 98 +++++++++++++++++++ examples/mouse/CMakeLists.txt | 2 + examples/mouse/main.cpp | 106 +++++++++++++++++++++ examples/primitives/CMakeLists.txt | 2 + examples/primitives/main.cpp | 109 +++++++++++++++++++++ 15 files changed, 642 insertions(+), 1 deletion(-) create mode 100644 examples/CMakeLists.txt create mode 100644 examples/actions/CMakeLists.txt create mode 100644 examples/actions/main.cpp create mode 100644 examples/controller/CMakeLists.txt create mode 100644 examples/controller/main.cpp create mode 100644 examples/keyboard/CMakeLists.txt create mode 100644 examples/keyboard/main.cpp create mode 100644 examples/mouse/CMakeLists.txt create mode 100644 examples/mouse/main.cpp create mode 100644 examples/primitives/CMakeLists.txt create mode 100644 examples/primitives/main.cpp diff --git a/.gitignore b/.gitignore index a6aa305..0173cd5 100644 --- a/.gitignore +++ b/.gitignore @@ -44,5 +44,6 @@ docs/ _deps/ .DS_Store build/ +bin/ .DS_Store \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 2f5a6ec..179cc8d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -65,6 +65,12 @@ target_link_libraries( z ) +# Examples +option(ASW_BUILD_EXAMPLES "Build ASW examples" OFF) +if(ASW_BUILD_EXAMPLES) + add_subdirectory(examples) +endif() + # Install include(CMakePackageConfigHelpers) diff --git a/Doxyfile b/Doxyfile index 69dcf8e..765a022 100644 --- a/Doxyfile +++ b/Doxyfile @@ -21,4 +21,4 @@ GENERATE_LATEX = NO HTML_OUTPUT = docs HAVE_DOT = YES DOT_IMAGE_FORMAT = svg -EXTRACT_ALL = YES \ No newline at end of file +EXTRACT_ALL = YES \ No newline at end of file diff --git a/README.md b/README.md index e75e539..687246f 100644 --- a/README.md +++ b/README.md @@ -29,3 +29,14 @@ FetchContent_MakeAvailable(asw) cmake --preset debug cmake --build --preset debug ``` + +Output is in the `lib/` directory. + +### Building Examples + +```sh +cmake --preset debug -DASW_BUILD_EXAMPLES=ON +cmake --build --preset debug +``` + +Output is in the `bin/` directory. diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt new file mode 100644 index 0000000..b15b0ec --- /dev/null +++ b/examples/CMakeLists.txt @@ -0,0 +1,7 @@ +cmake_minimum_required(VERSION 3.22) + +add_subdirectory(keyboard) +add_subdirectory(mouse) +add_subdirectory(controller) +add_subdirectory(actions) +add_subdirectory(primitives) diff --git a/examples/actions/CMakeLists.txt b/examples/actions/CMakeLists.txt new file mode 100644 index 0000000..b50a04a --- /dev/null +++ b/examples/actions/CMakeLists.txt @@ -0,0 +1,2 @@ +add_executable(example_actions main.cpp) +target_link_libraries(example_actions PRIVATE asw::asw) diff --git a/examples/actions/main.cpp b/examples/actions/main.cpp new file mode 100644 index 0000000..2f7d391 --- /dev/null +++ b/examples/actions/main.cpp @@ -0,0 +1,146 @@ +/// @file main.cpp +/// @brief Action binding example +/// +/// Demonstrates: +/// - Registering named actions with bind_action() +/// - Binding the same action to multiple input sources (keyboard + controller) +/// - is_action_down(), is_action_pressed(), is_action_released() +/// - get_action_strength() for analogue movement via a controller axis +/// +/// Bindings: +/// "move_left" → A key, Left arrow, controller left axis (negative X) +/// "move_right" → D key, Right arrow, controller left axis (positive X) +/// "move_up" → W key, Up arrow, controller left axis (negative Y) +/// "move_down" → S key, Down arrow, controller left axis (positive Y) +/// "fire" → Space, controller A button +/// +/// Controls: +/// WASD / arrows / left stick - move the box +/// Space / A button - fire (flash indicator) +/// Escape - quit + +#include + +int main() +{ + asw::core::init(800, 600); + asw::display::set_title("ASW Example - Action Bindings"); + + // --- Register actions --- + asw::input::bind_action("move_left", asw::input::KeyBinding { asw::input::Key::A }); + asw::input::bind_action("move_left", asw::input::KeyBinding { asw::input::Key::Left }); + asw::input::bind_action("move_left", + asw::input::ControllerAxisBinding { asw::input::ControllerAxis::LeftX, 0, 0.2F, false }); + + asw::input::bind_action("move_right", asw::input::KeyBinding { asw::input::Key::D }); + asw::input::bind_action("move_right", asw::input::KeyBinding { asw::input::Key::Right }); + asw::input::bind_action("move_right", + asw::input::ControllerAxisBinding { asw::input::ControllerAxis::LeftX, 0, 0.2F, true }); + + asw::input::bind_action("move_up", asw::input::KeyBinding { asw::input::Key::W }); + asw::input::bind_action("move_up", asw::input::KeyBinding { asw::input::Key::Up }); + asw::input::bind_action("move_up", + asw::input::ControllerAxisBinding { asw::input::ControllerAxis::LeftY, 0, 0.2F, false }); + + asw::input::bind_action("move_down", asw::input::KeyBinding { asw::input::Key::S }); + asw::input::bind_action("move_down", asw::input::KeyBinding { asw::input::Key::Down }); + asw::input::bind_action("move_down", + asw::input::ControllerAxisBinding { asw::input::ControllerAxis::LeftY, 0, 0.2F, true }); + + asw::input::bind_action("fire", asw::input::KeyBinding { asw::input::Key::Space }); + asw::input::bind_action( + "fire", asw::input::ControllerButtonBinding { asw::input::ControllerButton::A }); + + asw::input::bind_action("quit", asw::input::KeyBinding { asw::input::Key::Escape }); + asw::input::bind_action( + "quit", asw::input::ControllerButtonBinding { asw::input::ControllerButton::Start }); + + asw::Vec2 pos { 375.0F, 275.0F }; + constexpr float box_size = 50.0F; + constexpr float base_speed = 4.0F; + + int fire_frames = 0; + + while (!asw::core::exit) { + asw::core::update(); + + if (asw::input::is_action_pressed("quit")) { + asw::core::exit = true; + } + + // Move using analogue strength so a controller stick gives smooth speed + pos.x -= asw::input::get_action_strength("move_left") * base_speed; + pos.x += asw::input::get_action_strength("move_right") * base_speed; + pos.y -= asw::input::get_action_strength("move_up") * base_speed; + pos.y += asw::input::get_action_strength("move_down") * base_speed; + + // Clamp to window + const auto win = asw::display::get_logical_size(); + if (pos.x < 0) { + pos.x = 0; + } + if (pos.y < 0) { + pos.y = 0; + } + if (pos.x + box_size > static_cast(win.x)) { + pos.x = static_cast(win.x) - box_size; + } + if (pos.y + box_size > static_cast(win.y)) { + pos.y = static_cast(win.y) - box_size; + } + + // Fire + if (asw::input::is_action_pressed("fire")) { + fire_frames = 12; + asw::log::info("Fire!"); + } + if (fire_frames > 0) { + --fire_frames; + } + + // --- Draw --- + asw::display::clear(asw::color::darkslategray); + + // Fire flash + if (fire_frames > 0) { + const float alpha = static_cast(fire_frames) / 12.0F; + asw::draw::circle_fill({ pos.x + box_size / 2.0F, pos.y + box_size / 2.0F }, + box_size * 1.5F, asw::Color(255, 200, 0, static_cast(alpha * 200))); + } + + // Player box + const bool moving = asw::input::is_action_down("move_left") + || asw::input::is_action_down("move_right") || asw::input::is_action_down("move_up") + || asw::input::is_action_down("move_down"); + asw::draw::rect_fill({ pos, { box_size, box_size } }, + moving ? asw::color::cornflowerblue : asw::color::steelblue); + asw::draw::rect({ pos, { box_size, box_size } }, asw::color::white); + + // Strength bars for left/right + const float sl = asw::input::get_action_strength("move_left"); + const float sr = asw::input::get_action_strength("move_right"); + const float su = asw::input::get_action_strength("move_up"); + const float sd = asw::input::get_action_strength("move_down"); + + constexpr float bar_w = 100.0F; + constexpr float bar_h = 14.0F; + const float by = static_cast(win.y) - 60.0F; + + asw::draw::rect({ 50.0F, by, bar_w, bar_h }, asw::color::gray); + asw::draw::rect_fill({ 50.0F, by, bar_w * sl, bar_h }, asw::color::cyan); + + asw::draw::rect({ 650.0F, by, bar_w, bar_h }, asw::color::gray); + asw::draw::rect_fill({ 650.0F, by, bar_w * sr, bar_h }, asw::color::cyan); + + asw::draw::rect({ 350.0F, by - 20.0F, bar_h, bar_w }, asw::color::gray); + asw::draw::rect_fill({ 350.0F, by - 20.0F, bar_h, bar_w * su }, asw::color::cyan); + + asw::draw::rect({ 380.0F, by - 20.0F, bar_h, bar_w }, asw::color::gray); + asw::draw::rect_fill({ 380.0F, by - 20.0F, bar_h, bar_w * sd }, asw::color::cyan); + + asw::display::present(); + } + + asw::input::clear_actions(); + return 0; +} diff --git a/examples/controller/CMakeLists.txt b/examples/controller/CMakeLists.txt new file mode 100644 index 0000000..c71f62b --- /dev/null +++ b/examples/controller/CMakeLists.txt @@ -0,0 +1,2 @@ +add_executable(example_controller main.cpp) +target_link_libraries(example_controller PRIVATE asw::asw) diff --git a/examples/controller/main.cpp b/examples/controller/main.cpp new file mode 100644 index 0000000..2fc3338 --- /dev/null +++ b/examples/controller/main.cpp @@ -0,0 +1,147 @@ +/// @file main.cpp +/// @brief Controller / gamepad input example +/// +/// Demonstrates: +/// - Detecting connected controllers via get_controller_count() +/// - Reading axes with get_controller_axis() +/// - Polling buttons with get_controller_button_down() / get_controller_button_up() / +/// get_controller_button() +/// - Setting the dead zone with set_controller_dead_zone() +/// - Visualising left/right sticks as dots inside circles +/// - Displaying button states as coloured indicators +/// +/// Controls: +/// Left stick - move left stick indicator +/// Right stick - move right stick indicator +/// Triggers - fill trigger bars +/// Any button - highlight corresponding indicator +/// Keyboard Escape - quit + +#include + +namespace { + +/// Draw a stick visualiser: a circle with a dot at the axis position. +void draw_stick(asw::Vec2 center, float radius, float axis_x, float axis_y) +{ + asw::draw::circle(center, radius, asw::color::gray); + asw::draw::circle_fill( + { center.x + axis_x * radius, center.y + axis_y * radius }, 10.0F, asw::color::cyan); +} + +/// Draw a horizontal trigger bar filled proportionally. +void draw_trigger(asw::Vec2 pos, asw::Vec2 size, float value) +{ + asw::draw::rect({ pos, size }, asw::color::gray); + asw::draw::rect_fill({ pos, { size.x * value, size.y } }, asw::color::orange); +} + +/// Draw a button indicator (coloured box). +void draw_button(asw::Vec2 pos, float size, asw::Color on_color, bool pressed) +{ + asw::draw::rect_fill({ pos, { size, size } }, pressed ? on_color : on_color.darken(0.7F)); + asw::draw::rect({ pos, { size, size } }, asw::color::gray); +} + +} // namespace + +int main() +{ + asw::core::init(800, 600); + asw::display::set_title("ASW Example - Controller"); + + // Use a comfortable dead zone + asw::input::set_controller_dead_zone(0, 0.15F); + + while (!asw::core::exit) { + asw::core::update(); + + if (asw::input::get_key_down(asw::input::Key::Escape)) { + asw::core::exit = true; + } + + // Log button events for controller 0 + if (asw::input::get_controller_button_down(0, asw::input::ControllerButton::A)) { + asw::log::info("A pressed"); + } + if (asw::input::get_controller_button_down(0, asw::input::ControllerButton::B)) { + asw::log::info("B pressed"); + } + if (asw::input::get_controller_button_down(0, asw::input::ControllerButton::X)) { + asw::log::info("X pressed"); + } + if (asw::input::get_controller_button_down(0, asw::input::ControllerButton::Y)) { + asw::log::info("Y pressed"); + } + if (asw::input::get_controller_button_down(0, asw::input::ControllerButton::Start)) { + asw::log::info("Start pressed"); + } + + // --- Draw --- + asw::display::clear(asw::color::darkslategray); + + const int count = asw::input::get_controller_count(); + if (count == 0) { + // No controller – show a simple message indicator + asw::draw::rect_fill({ 250.0F, 270.0F, 300.0F, 60.0F }, asw::color::darkred); + asw::draw::rect({ 250.0F, 270.0F, 300.0F, 60.0F }, asw::color::red); + } else { + // Read axes for controller 0 + const float lx = asw::input::get_controller_axis(0, asw::input::ControllerAxis::LeftX); + const float ly = asw::input::get_controller_axis(0, asw::input::ControllerAxis::LeftY); + const float rx = asw::input::get_controller_axis(0, asw::input::ControllerAxis::RightX); + const float ry = asw::input::get_controller_axis(0, asw::input::ControllerAxis::RightY); + const float lt + = asw::input::get_controller_axis(0, asw::input::ControllerAxis::LeftTrigger); + const float rt + = asw::input::get_controller_axis(0, asw::input::ControllerAxis::RightTrigger); + + // Sticks + draw_stick({ 200.0F, 350.0F }, 80.0F, lx, ly); + draw_stick({ 500.0F, 420.0F }, 60.0F, rx, ry); + + // Triggers + draw_trigger({ 100.0F, 480.0F }, { 120.0F, 20.0F }, (lt + 1.0F) / 2.0F); + draw_trigger({ 580.0F, 480.0F }, { 120.0F, 20.0F }, (rt + 1.0F) / 2.0F); + + // Face buttons (A/B/X/Y) - diamond layout + constexpr float bs = 36.0F; + const asw::Vec2 face { 570.0F, 260.0F }; + draw_button({ face.x + bs, face.y }, bs, asw::color::yellow, + asw::input::get_controller_button(0, asw::input::ControllerButton::Y)); + draw_button({ face.x, face.y + bs }, bs, asw::color::blue, + asw::input::get_controller_button(0, asw::input::ControllerButton::X)); + draw_button({ face.x + bs * 2, face.y + bs }, bs, asw::color::red, + asw::input::get_controller_button(0, asw::input::ControllerButton::B)); + draw_button({ face.x + bs, face.y + bs * 2 }, bs, asw::color::lime, + asw::input::get_controller_button(0, asw::input::ControllerButton::A)); + + // Shoulder buttons + draw_button({ 100.0F, 200.0F }, bs, asw::color::purple, + asw::input::get_controller_button(0, asw::input::ControllerButton::LeftShoulder)); + draw_button({ 664.0F, 200.0F }, bs, asw::color::purple, + asw::input::get_controller_button(0, asw::input::ControllerButton::RightShoulder)); + + // D-Pad + const asw::Vec2 dpad { 310.0F, 420.0F }; + draw_button({ dpad.x + bs, dpad.y }, bs, asw::color::silver, + asw::input::get_controller_button(0, asw::input::ControllerButton::DPadUp)); + draw_button({ dpad.x, dpad.y + bs }, bs, asw::color::silver, + asw::input::get_controller_button(0, asw::input::ControllerButton::DPadLeft)); + draw_button({ dpad.x + bs * 2, dpad.y + bs }, bs, asw::color::silver, + asw::input::get_controller_button(0, asw::input::ControllerButton::DPadRight)); + draw_button({ dpad.x + bs, dpad.y + bs * 2 }, bs, asw::color::silver, + asw::input::get_controller_button(0, asw::input::ControllerButton::DPadDown)); + + // Start / Back + draw_button({ 370.0F, 260.0F }, bs, asw::color::silver, + asw::input::get_controller_button(0, asw::input::ControllerButton::Start)); + draw_button({ 310.0F, 260.0F }, bs, asw::color::silver, + asw::input::get_controller_button(0, asw::input::ControllerButton::Back)); + } + + asw::display::present(); + } + + return 0; +} diff --git a/examples/keyboard/CMakeLists.txt b/examples/keyboard/CMakeLists.txt new file mode 100644 index 0000000..b8fea66 --- /dev/null +++ b/examples/keyboard/CMakeLists.txt @@ -0,0 +1,2 @@ +add_executable(example_keyboard main.cpp) +target_link_libraries(example_keyboard PRIVATE asw::asw) diff --git a/examples/keyboard/main.cpp b/examples/keyboard/main.cpp new file mode 100644 index 0000000..aafaf4c --- /dev/null +++ b/examples/keyboard/main.cpp @@ -0,0 +1,98 @@ +/// @file main.cpp +/// @brief Keyboard input example +/// +/// Demonstrates: +/// - Polling keys with get_key() (held), get_key_down() (pressed), get_key_up() (released) +/// - Moving a box with WASD / arrow keys +/// - Detecting individual key events and logging them +/// - Reading text_input for typed characters +/// +/// Controls: +/// WASD / Arrow keys - move the box +/// Escape - quit + +#include + +int main() +{ + asw::core::init(800, 600); + asw::display::set_title("ASW Example - Keyboard"); + + asw::Vec2 pos { 375.0F, 275.0F }; + constexpr float speed = 3.0F; + constexpr float box_size = 50.0F; + + while (!asw::core::exit) { + asw::core::update(); + + // --- Movement (held) --- + if (asw::input::get_key(asw::input::Key::W) || asw::input::get_key(asw::input::Key::Up)) { + pos.y -= speed; + } + if (asw::input::get_key(asw::input::Key::S) || asw::input::get_key(asw::input::Key::Down)) { + pos.y += speed; + } + if (asw::input::get_key(asw::input::Key::A) || asw::input::get_key(asw::input::Key::Left)) { + pos.x -= speed; + } + if (asw::input::get_key(asw::input::Key::D) + || asw::input::get_key(asw::input::Key::Right)) { + pos.x += speed; + } + + // Clamp to window + const auto win = asw::display::get_logical_size(); + if (pos.x < 0) + pos.x = 0; + if (pos.y < 0) + pos.y = 0; + if (pos.x + box_size > static_cast(win.x)) + pos.x = static_cast(win.x) - box_size; + if (pos.y + box_size > static_cast(win.y)) + pos.y = static_cast(win.y) - box_size; + + // --- Key-press events (single frame) --- + if (asw::input::get_key_down(asw::input::Key::Space)) { + asw::log::info("Space pressed"); + } + if (asw::input::get_key_up(asw::input::Key::Space)) { + asw::log::info("Space released"); + } + if (asw::input::get_key_down(asw::input::Key::Escape)) { + asw::core::exit = true; + } + + // --- Text input --- + if (!asw::input::text_input.empty()) { + asw::log::info("Text input: {}", asw::input::text_input); + } + + // --- Draw --- + asw::display::clear(asw::color::darkslategray); + + // Box - turns cyan when any key is held + const bool any_held = asw::input::keyboard.any_pressed; + asw::draw::rect_fill( + { pos, { box_size, box_size } }, any_held ? asw::color::cyan : asw::color::white); + asw::draw::rect({ pos, { box_size, box_size } }, asw::color::gray); + + // Indicator dots for WASD + const asw::Vec2 center { 400.0F, 530.0F }; + constexpr float dot = 12.0F; + const auto key_color + = [](bool down) { return down ? asw::color::lime : asw::color::dimgray; }; + + asw::draw::circle_fill({ center.x, center.y - 20.0F }, dot, + key_color(asw::input::get_key(asw::input::Key::W))); + asw::draw::circle_fill({ center.x, center.y + 20.0F }, dot, + key_color(asw::input::get_key(asw::input::Key::S))); + asw::draw::circle_fill({ center.x - 20.0F, center.y }, dot, + key_color(asw::input::get_key(asw::input::Key::A))); + asw::draw::circle_fill({ center.x + 20.0F, center.y }, dot, + key_color(asw::input::get_key(asw::input::Key::D))); + + asw::display::present(); + } + + return 0; +} diff --git a/examples/mouse/CMakeLists.txt b/examples/mouse/CMakeLists.txt new file mode 100644 index 0000000..b7aba4f --- /dev/null +++ b/examples/mouse/CMakeLists.txt @@ -0,0 +1,2 @@ +add_executable(example_mouse main.cpp) +target_link_libraries(example_mouse PRIVATE asw::asw) diff --git a/examples/mouse/main.cpp b/examples/mouse/main.cpp new file mode 100644 index 0000000..f55c093 --- /dev/null +++ b/examples/mouse/main.cpp @@ -0,0 +1,106 @@ +/// @file main.cpp +/// @brief Mouse input example +/// +/// Demonstrates: +/// - Reading mouse position and delta via asw::input::mouse +/// - Polling buttons with get_mouse_button_down() / get_mouse_button_up() / get_mouse_button() +/// - Scroll wheel via mouse.z +/// - Changing cursor style with set_cursor() +/// +/// Controls: +/// Move mouse - move the cursor circle +/// Left click - change circle color to red +/// Right click - change circle color to blue +/// Scroll wheel - resize the circle +/// Escape - quit + +#include +#include + +int main() +{ + asw::core::init(800, 600); + asw::display::set_title("ASW Example - Mouse"); + + asw::Color circle_color = asw::color::white; + float radius = 30.0F; + + while (!asw::core::exit) { + asw::core::update(); + + // --- Button events --- + if (asw::input::get_mouse_button_down(asw::input::MouseButton::Left)) { + circle_color = asw::color::red; + asw::log::info("Left button pressed at ({:.0f}, {:.0f})", asw::input::mouse.position.x, + asw::input::mouse.position.y); + } + if (asw::input::get_mouse_button_down(asw::input::MouseButton::Right)) { + circle_color = asw::color::blue; + asw::log::info("Right button pressed at ({:.0f}, {:.0f})", asw::input::mouse.position.x, + asw::input::mouse.position.y); + } + if (asw::input::get_mouse_button_up(asw::input::MouseButton::Left) + || asw::input::get_mouse_button_up(asw::input::MouseButton::Right)) { + circle_color = asw::color::white; + } + + // --- Scroll wheel --- + if (asw::input::mouse.z != 0.0F) { + radius = std::clamp(radius + asw::input::mouse.z * 5.0F, 5.0F, 150.0F); + asw::log::info("Scroll: {:.1f} radius: {:.0f}", asw::input::mouse.z, radius); + } + + // --- Cursor style based on button held --- + if (asw::input::get_mouse_button(asw::input::MouseButton::Left)) { + asw::input::set_cursor(asw::input::CursorId::Crosshair); + } else if (asw::input::get_mouse_button(asw::input::MouseButton::Right)) { + asw::input::set_cursor(asw::input::CursorId::Move); + } else { + asw::input::set_cursor(asw::input::CursorId::Default); + } + + if (asw::input::get_key_down(asw::input::Key::Escape)) { + asw::core::exit = true; + } + + // --- Draw --- + asw::display::clear(asw::color::darkslategray); + + const auto& mp = asw::input::mouse.position; + + // Crosshair lines + const auto win = asw::display::get_logical_size(); + asw::draw::line({ 0.0F, mp.y }, { static_cast(win.x), mp.y }, asw::color::dimgray); + asw::draw::line({ mp.x, 0.0F }, { mp.x, static_cast(win.y) }, asw::color::dimgray); + + // Main cursor circle + asw::draw::circle_fill(mp, radius, circle_color.with_alpha(180)); + asw::draw::circle(mp, radius, asw::color::white); + + // Delta indicator (shows mouse movement direction) + const auto& delta = asw::input::mouse.change; + if (delta.x != 0.0F || delta.y != 0.0F) { + asw::draw::line( + mp, { mp.x + delta.x * 10.0F, mp.y + delta.y * 10.0F }, asw::color::yellow); + } + + // Button state indicators + constexpr float btn_size = 30.0F; + const asw::Vec2 btn_base { 20.0F, 20.0F }; + asw::draw::rect_fill({ btn_base, { btn_size, btn_size } }, + asw::input::get_mouse_button(asw::input::MouseButton::Left) ? asw::color::red + : asw::color::darkred); + asw::draw::rect_fill( + { { btn_base.x + btn_size + 5.0F, btn_base.y }, { btn_size, btn_size } }, + asw::input::get_mouse_button(asw::input::MouseButton::Middle) ? asw::color::lime + : asw::color::darkgreen); + asw::draw::rect_fill( + { { btn_base.x + (btn_size + 5.0F) * 2.0F, btn_base.y }, { btn_size, btn_size } }, + asw::input::get_mouse_button(asw::input::MouseButton::Right) ? asw::color::blue + : asw::color::darkblue); + + asw::display::present(); + } + + return 0; +} diff --git a/examples/primitives/CMakeLists.txt b/examples/primitives/CMakeLists.txt new file mode 100644 index 0000000..b6f2e99 --- /dev/null +++ b/examples/primitives/CMakeLists.txt @@ -0,0 +1,2 @@ +add_executable(example_primitives main.cpp) +target_link_libraries(example_primitives PRIVATE asw::asw) diff --git a/examples/primitives/main.cpp b/examples/primitives/main.cpp new file mode 100644 index 0000000..85798f7 --- /dev/null +++ b/examples/primitives/main.cpp @@ -0,0 +1,109 @@ +/// @file main.cpp +/// @brief Drawing primitives example +/// +/// Demonstrates: +/// - asw::draw::point() +/// - asw::draw::line() +/// - asw::draw::rect() and rect_fill() +/// - asw::draw::circle() and circle_fill() +/// - asw::draw::clear_color() +/// - asw::color constants and Color utilities (lighten, darken, with_alpha) +/// - asw::display::clear(), present() +/// +/// Controls: +/// Escape - quit + +#include +#include + +int main() +{ + asw::core::init(800, 600); + asw::display::set_title("ASW Example - Primitives"); + + float angle = 0.0F; + + while (!asw::core::exit) { + asw::core::update(); + + if (asw::input::get_key_down(asw::input::Key::Escape)) { + asw::core::exit = true; + } + + angle += 0.02F; + + asw::display::clear(asw::color::darkslategray); + + // --- Points --- + for (int i = 0; i < 20; ++i) { + const float x = 40.0F + static_cast(i) * 8.0F; + asw::draw::point({ x, 30.0F }, asw::color::white); + } + + // --- Lines --- + asw::draw::line({ 50.0F, 60.0F }, { 350.0F, 60.0F }, asw::color::lime); + asw::draw::line({ 50.0F, 80.0F }, { 350.0F, 80.0F }, asw::color::lime.darken(0.4F)); + + // Animated spoke lines from a centre point + const asw::Vec2 hub { 400.0F, 150.0F }; + constexpr int spokes = 8; + for (int i = 0; i < spokes; ++i) { + const float a = angle + static_cast(i) * (3.14159265F * 2.0F / spokes); + const float len = 60.0F; + asw::draw::line(hub, { hub.x + std::cos(a) * len, hub.y + std::sin(a) * len }, + asw::Color(200, 200, static_cast(128 + 127 * std::sin(a)))); + } + + // --- Rectangles --- + asw::draw::rect({ 50.0F, 120.0F, 120.0F, 70.0F }, asw::color::red); + asw::draw::rect_fill({ 200.0F, 120.0F, 120.0F, 70.0F }, asw::color::red.darken(0.3F)); + asw::draw::rect({ 200.0F, 120.0F, 120.0F, 70.0F }, asw::color::red); + + // Colour gradient row of filled rects + for (int i = 0; i < 8; ++i) { + const float t = static_cast(i) / 7.0F; + const asw::Color c { static_cast(255 * t), static_cast(100), + static_cast(255 * (1.0F - t)) }; + asw::draw::rect_fill( + { 50.0F + static_cast(i) * 40.0F, 220.0F, 35.0F, 35.0F }, c); + } + + // --- Circles --- + asw::draw::circle({ 100.0F, 340.0F }, 50.0F, asw::color::yellow); + asw::draw::circle_fill({ 220.0F, 340.0F }, 50.0F, asw::color::yellow.darken(0.3F)); + asw::draw::circle({ 220.0F, 340.0F }, 50.0F, asw::color::yellow); + + // Concentric rings + const asw::Vec2 rings { 400.0F, 400.0F }; + for (int i = 5; i >= 1; --i) { + const float r = static_cast(i) * 25.0F; + const uint8_t alpha = static_cast(255 * (static_cast(i) / 5.0F)); + asw::draw::circle_fill(rings, r, asw::color::cornflowerblue.with_alpha(alpha)); + } + asw::draw::circle(rings, 125.0F, asw::color::white); + + // Animated orbit circle + const float ox = rings.x + std::cos(angle * 2.0F) * 100.0F; + const float oy = rings.y + std::sin(angle * 2.0F) * 100.0F; + asw::draw::circle_fill({ ox, oy }, 15.0F, asw::color::orange); + asw::draw::circle({ ox, oy }, 15.0F, asw::color::white); + + // --- Alpha / transparency demo --- + asw::draw::rect_fill({ 550.0F, 100.0F, 200.0F, 200.0F }, asw::color::purple); + for (int i = 0; i < 5; ++i) { + const uint8_t a = static_cast(50 + i * 40); + asw::draw::rect_fill({ 560.0F + static_cast(i) * 30.0F, 110.0F, 25.0F, 180.0F }, + asw::color::white.with_alpha(a)); + } + + // Border + const auto win = asw::display::get_logical_size(); + asw::draw::rect( + { 2.0F, 2.0F, static_cast(win.x) - 4.0F, static_cast(win.y) - 4.0F }, + asw::color::gray); + + asw::display::present(); + } + + return 0; +} From 27e97d60d82c2dd33a63342512799397fb9dd829 Mon Sep 17 00:00:00 2001 From: Allan Legemaate Date: Sat, 14 Mar 2026 11:07:57 -0400 Subject: [PATCH 3/8] feat: add examples to build --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index eaf45c6..829e7b2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -35,8 +35,8 @@ jobs: run: | embuilder build harfbuzz - - name: Run CMake - run: emcmake cmake --preset release + - name: Run CMake (Release & Examples) + run: emcmake cmake --preset release -DASW_BUILD_EXAMPLES=ON - name: Make run: cmake --build --preset release From 8370ef4b07f23eb8e06523531d7c7753db667148 Mon Sep 17 00:00:00 2001 From: Allan Legemaate Date: Sat, 14 Mar 2026 11:10:36 -0400 Subject: [PATCH 4/8] fix: caching --- .github/workflows/build.yml | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 829e7b2..6251212 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,6 +16,9 @@ jobs: build: name: Test Build runs-on: ubuntu-latest + env: + EM_VERSION: 5.0.3 + EM_CACHE_FOLDER: "emsdk-cache" steps: - name: Checkout uses: actions/checkout@v4 @@ -23,13 +26,13 @@ jobs: - name: Setup cache uses: actions/cache@v4 with: - path: "_deps" - key: deps-${{ runner.os }} + path: ${{ env.EM_CACHE_FOLDER }} + key: ${{ env.EM_VERSION }}-${{ runner.os }} - uses: mymindstorm/setup-emsdk@v14 with: - version: 4.0.6 - actions-cache-folder: "emsdk-cache" + version: ${{ env.EM_VERSION }} + actions-cache-folder: ${{ env.EM_CACHE_FOLDER }} - name: Download ports run: | From 1124854a08dc5b3076063aad200f9f0b566f950f Mon Sep 17 00:00:00 2001 From: Allan Legemaate Date: Sat, 14 Mar 2026 17:55:04 -0400 Subject: [PATCH 5/8] chore: update libs --- CMakeLists.txt | 9 ++- examples/actions/main.cpp | 6 +- examples/controller/main.cpp | 21 +++++- examples/keyboard/main.cpp | 18 +++-- examples/mouse/main.cpp | 6 +- examples/primitives/main.cpp | 25 ++++--- include/asw/modules/core.h | 12 +++- include/asw/modules/input.h | 26 +++++-- include/asw/modules/scene.h | 10 ++- include/asw/modules/sound.h | 31 ++++----- include/asw/modules/types.h | 8 +-- src/modules/action.cpp | 26 +++---- src/modules/assets.cpp | 9 +-- src/modules/core.cpp | 94 +++++++++---------------- src/modules/input.cpp | 130 ++++++++++++++++++++++++++++++---- src/modules/sound.cpp | 131 +++++++++++++++++++++++++++-------- 16 files changed, 379 insertions(+), 183 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 179cc8d..bf3845f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -34,11 +34,10 @@ if(TARGET freetype AND NOT TARGET Freetype::Freetype) add_library(Freetype::Freetype ALIAS freetype) endif() -CPMAddPackage("gh:libsdl-org/SDL#release-3.2.8") -CPMAddPackage("gh:libsdl-org/SDL_image#release-3.2.4") -CPMAddPackage("gh:libsdl-org/SDL_ttf#release-3.2.0") -# V3 not released yet -CPMAddPackage("gh:libsdl-org/SDL_mixer#a83eb9c59c55b48da72dc7b062c78d8dc52ec322") +CPMAddPackage("gh:libsdl-org/SDL#release-3.4.2") +CPMAddPackage("gh:libsdl-org/SDL_image#release-3.4.0") +CPMAddPackage("gh:libsdl-org/SDL_ttf#release-3.2.2") +CPMAddPackage("gh:libsdl-org/SDL_mixer#release-3.2.0") # Add include target_include_directories( diff --git a/examples/actions/main.cpp b/examples/actions/main.cpp index 2f7d391..3927920 100644 --- a/examples/actions/main.cpp +++ b/examples/actions/main.cpp @@ -25,6 +25,7 @@ int main() { asw::core::init(800, 600); asw::display::set_title("ASW Example - Action Bindings"); + asw::core::print_info(); // --- Register actions --- asw::input::bind_action("move_left", asw::input::KeyBinding { asw::input::Key::A }); @@ -61,11 +62,12 @@ int main() int fire_frames = 0; - while (!asw::core::exit) { + while (!asw::core::is_exiting()) { asw::core::update(); if (asw::input::is_action_pressed("quit")) { - asw::core::exit = true; + asw::core::exit(); + break; } // Move using analogue strength so a controller stick gives smooth speed diff --git a/examples/controller/main.cpp b/examples/controller/main.cpp index 2fc3338..4b864ad 100644 --- a/examples/controller/main.cpp +++ b/examples/controller/main.cpp @@ -49,15 +49,28 @@ int main() { asw::core::init(800, 600); asw::display::set_title("ASW Example - Controller"); + asw::core::print_info(); // Use a comfortable dead zone asw::input::set_controller_dead_zone(0, 0.15F); - while (!asw::core::exit) { + // Get controller name + const auto count = asw::input::get_controller_count(); + asw::log::info("Controller count: {}", count); + + const auto name = asw::input::get_controller_name(0); + if (!name.empty()) { + asw::log::info("Controller 0 connected: {}", name); + } else { + asw::log::info("No controllers detected"); + } + + while (!asw::core::is_exiting()) { asw::core::update(); if (asw::input::get_key_down(asw::input::Key::Escape)) { - asw::core::exit = true; + asw::core::exit(); + break; } // Log button events for controller 0 @@ -96,6 +109,10 @@ int main() const float rt = asw::input::get_controller_axis(0, asw::input::ControllerAxis::RightTrigger); + asw::log::info("Left stick: ({:.2f}, {:.2f}), Right stick: ({:.2f}, {:.2f}), Triggers: " + "L {:.2f}, R {:.2f}", + lx, ly, rx, ry, lt, rt); + // Sticks draw_stick({ 200.0F, 350.0F }, 80.0F, lx, ly); draw_stick({ 500.0F, 420.0F }, 60.0F, rx, ry); diff --git a/examples/keyboard/main.cpp b/examples/keyboard/main.cpp index aafaf4c..5d6391c 100644 --- a/examples/keyboard/main.cpp +++ b/examples/keyboard/main.cpp @@ -17,12 +17,13 @@ int main() { asw::core::init(800, 600); asw::display::set_title("ASW Example - Keyboard"); + asw::core::print_info(); asw::Vec2 pos { 375.0F, 275.0F }; constexpr float speed = 3.0F; constexpr float box_size = 50.0F; - while (!asw::core::exit) { + while (!asw::core::is_exiting()) { asw::core::update(); // --- Movement (held) --- @@ -42,14 +43,18 @@ int main() // Clamp to window const auto win = asw::display::get_logical_size(); - if (pos.x < 0) + if (pos.x < 0) { pos.x = 0; - if (pos.y < 0) + } + if (pos.y < 0) { pos.y = 0; - if (pos.x + box_size > static_cast(win.x)) + } + if (pos.x + box_size > static_cast(win.x)) { pos.x = static_cast(win.x) - box_size; - if (pos.y + box_size > static_cast(win.y)) + } + if (pos.y + box_size > static_cast(win.y)) { pos.y = static_cast(win.y) - box_size; + } // --- Key-press events (single frame) --- if (asw::input::get_key_down(asw::input::Key::Space)) { @@ -59,7 +64,8 @@ int main() asw::log::info("Space released"); } if (asw::input::get_key_down(asw::input::Key::Escape)) { - asw::core::exit = true; + asw::core::exit(); + break; } // --- Text input --- diff --git a/examples/mouse/main.cpp b/examples/mouse/main.cpp index f55c093..f7c1573 100644 --- a/examples/mouse/main.cpp +++ b/examples/mouse/main.cpp @@ -21,11 +21,12 @@ int main() { asw::core::init(800, 600); asw::display::set_title("ASW Example - Mouse"); + asw::core::print_info(); asw::Color circle_color = asw::color::white; float radius = 30.0F; - while (!asw::core::exit) { + while (!asw::core::is_exiting()) { asw::core::update(); // --- Button events --- @@ -60,7 +61,8 @@ int main() } if (asw::input::get_key_down(asw::input::Key::Escape)) { - asw::core::exit = true; + asw::core::exit(); + break; } // --- Draw --- diff --git a/examples/primitives/main.cpp b/examples/primitives/main.cpp index 85798f7..e622a78 100644 --- a/examples/primitives/main.cpp +++ b/examples/primitives/main.cpp @@ -20,14 +20,16 @@ int main() { asw::core::init(800, 600); asw::display::set_title("ASW Example - Primitives"); + asw::core::print_info(); float angle = 0.0F; - while (!asw::core::exit) { + while (!asw::core::is_exiting()) { asw::core::update(); if (asw::input::get_key_down(asw::input::Key::Escape)) { - asw::core::exit = true; + asw::core::exit(); + break; } angle += 0.02F; @@ -36,7 +38,7 @@ int main() // --- Points --- for (int i = 0; i < 20; ++i) { - const float x = 40.0F + static_cast(i) * 8.0F; + const float x = 40.0F + (static_cast(i) * 8.0F); asw::draw::point({ x, 30.0F }, asw::color::white); } @@ -48,10 +50,10 @@ int main() const asw::Vec2 hub { 400.0F, 150.0F }; constexpr int spokes = 8; for (int i = 0; i < spokes; ++i) { - const float a = angle + static_cast(i) * (3.14159265F * 2.0F / spokes); + const float a = angle + (static_cast(i) * (3.14159265F * 2.0F / spokes)); const float len = 60.0F; asw::draw::line(hub, { hub.x + std::cos(a) * len, hub.y + std::sin(a) * len }, - asw::Color(200, 200, static_cast(128 + 127 * std::sin(a)))); + asw::Color(200, 200, static_cast(128 + (127 * std::sin(a))))); } // --- Rectangles --- @@ -65,7 +67,7 @@ int main() const asw::Color c { static_cast(255 * t), static_cast(100), static_cast(255 * (1.0F - t)) }; asw::draw::rect_fill( - { 50.0F + static_cast(i) * 40.0F, 220.0F, 35.0F, 35.0F }, c); + { 50.0F + (static_cast(i) * 40.0F), 220.0F, 35.0F, 35.0F }, c); } // --- Circles --- @@ -77,22 +79,23 @@ int main() const asw::Vec2 rings { 400.0F, 400.0F }; for (int i = 5; i >= 1; --i) { const float r = static_cast(i) * 25.0F; - const uint8_t alpha = static_cast(255 * (static_cast(i) / 5.0F)); + const auto alpha = static_cast(255 * (static_cast(i) / 5.0F)); asw::draw::circle_fill(rings, r, asw::color::cornflowerblue.with_alpha(alpha)); } asw::draw::circle(rings, 125.0F, asw::color::white); // Animated orbit circle - const float ox = rings.x + std::cos(angle * 2.0F) * 100.0F; - const float oy = rings.y + std::sin(angle * 2.0F) * 100.0F; + const float ox = rings.x + (std::cos(angle * 2.0F) * 100.0F); + const float oy = rings.y + (std::sin(angle * 2.0F) * 100.0F); asw::draw::circle_fill({ ox, oy }, 15.0F, asw::color::orange); asw::draw::circle({ ox, oy }, 15.0F, asw::color::white); // --- Alpha / transparency demo --- asw::draw::rect_fill({ 550.0F, 100.0F, 200.0F, 200.0F }, asw::color::purple); for (int i = 0; i < 5; ++i) { - const uint8_t a = static_cast(50 + i * 40); - asw::draw::rect_fill({ 560.0F + static_cast(i) * 30.0F, 110.0F, 25.0F, 180.0F }, + const auto a = static_cast(50 + i * 40); + asw::draw::rect_fill( + { 560.0F + (static_cast(i) * 30.0F), 110.0F, 25.0F, 180.0F }, asw::color::white.with_alpha(a)); } diff --git a/include/asw/modules/core.h b/include/asw/modules/core.h index 8eacfbc..e5e6bf4 100644 --- a/include/asw/modules/core.h +++ b/include/asw/modules/core.h @@ -11,9 +11,6 @@ namespace asw::core { -/// @brief When set to true, exits the main loop. -extern bool exit; - /// @brief Updates core module functionality. /// void update(); @@ -30,6 +27,15 @@ void init(int width, int height, int scale = 1); /// void print_info(); +/// @brief Exit the application. +/// Calls SDL_Quit() and performs any necessary cleanup. +/// +void exit(); + +/// @brief Return exiting status. +/// +bool is_exiting(); + } // namespace asw::core #endif // ASW_CORE_H \ No newline at end of file diff --git a/include/asw/modules/input.h b/include/asw/modules/input.h index 22bf995..1d6b922 100644 --- a/include/asw/modules/input.h +++ b/include/asw/modules/input.h @@ -456,17 +456,35 @@ using ControllerState = struct ControllerState { float dead_zone { 0.25F }; std::array axis { 0 }; + + SDL_Gamepad* gamepad { nullptr }; + std::string name; }; /** - * @brief Maximum number of controllers supported + * @brief Controller added hook + */ +void _controller_added(SDL_JoystickID id); + +/** + * @brief Controller removed hook + */ +void _controller_removed(SDL_JoystickID id); + +/** + * @brief Axis moved hook + */ +void _controller_axis_motion(SDL_JoystickID id, uint32_t axis, float value); + +/** + * @brief Button down hook */ -constexpr int MAX_CONTROLLERS = 8; +void _controller_button_down(SDL_JoystickID id, uint32_t button); /** - * @brief Global controller state. + * @brief Button up hook */ -extern std::array controller; +void _controller_button_up(SDL_JoystickID id, uint32_t button); /** * @brief Check if a controller button is down. diff --git a/include/asw/modules/scene.h b/include/asw/modules/scene.h index 4c10122..9806c15 100644 --- a/include/asw/modules/scene.h +++ b/include/asw/modules/scene.h @@ -243,7 +243,7 @@ template class SceneManager { auto last_second = std::chrono::high_resolution_clock::now(); int frames = 0; - while (!asw::core::exit) { + while (!asw::core::is_exiting()) { auto delta_time = std::chrono::high_resolution_clock::now() - time_start; time_start = std::chrono::high_resolution_clock::now(); lag += std::chrono::duration_cast(delta_time); @@ -273,6 +273,10 @@ template class SceneManager { /// void update(const float dt) { + if (asw::core::is_exiting()) { + return; + } + asw::core::update(); change_scene(); @@ -285,6 +289,10 @@ template class SceneManager { /// void draw() { + if (asw::core::is_exiting()) { + return; + } + if (active_scene_ != nullptr) { asw::display::clear(); active_scene_->draw(); diff --git a/include/asw/modules/sound.h b/include/asw/modules/sound.h index f770f1c..64c2c27 100644 --- a/include/asw/modules/sound.h +++ b/include/asw/modules/sound.h @@ -12,6 +12,18 @@ #include "./types.h" namespace asw::sound { + +/// @brief Global mixer device +extern MIX_Mixer* mixer; + +/// @brief Initialize the sound module. Must be called before using any other +/// sound functions. This is called automatically by asw::core::init(), +/// so you don't need to call it +/// +/// @return True if initialization was successful, false otherwise. +/// +bool _init(); + /// @brief Play a sample. /// /// @param sample Sample to play @@ -27,26 +39,13 @@ void play(const asw::Sample& sample, float volume = 1.0F, float pan = 0.0F, bool /// /// @param sample Sample to play /// @param volume Playback volume (0.0 - 1.0). +/// @param fade_in_s Fade-in duration in seconds. /// -void play_music(const asw::Music& sample, float volume = 1.0F); +void play_music(const asw::Music& sample, float volume = 1.0F, float fade_in_s = 0.0F); /// @brief Stop the currently playing music. /// -void stop_music(); - -/// @brief Fade in music over a duration. -/// -/// @param music The music to play. -/// @param volume Playback volume (0.0 - 1.0). -/// @param duration Fade duration in seconds. -/// -void fade_in_music(const asw::Music& music, float volume, float duration); - -/// @brief Fade out the currently playing music. -/// -/// @param duration Fade duration in seconds. -/// -void fade_out_music(float duration); +void stop_music(float fade_out_s = 0.0F); /// @brief Pause the currently playing music. /// diff --git a/include/asw/modules/types.h b/include/asw/modules/types.h index 044ea23..55b6cd9 100644 --- a/include/asw/modules/types.h +++ b/include/asw/modules/types.h @@ -40,11 +40,11 @@ using Texture = std::shared_ptr; /// @brief Alias for a shared pointer to an TTF_Font using Font = std::shared_ptr; -/// @brief Alias for a shared pointer to an Mix_Chunk -using Sample = std::shared_ptr; +/// @brief Alias for a shared pointer to an MIX_Audio +using Sample = std::shared_ptr; -/// @brief Alias for a shared pointer to an Mix_Music -using Music = std::shared_ptr; +/// @brief Alias for a shared pointer to an MIX_Audio +using Music = std::shared_ptr; /// @brief Alias for a shared pointer to an SDL_Renderer using Renderer = SDL_Renderer; diff --git a/src/modules/action.cpp b/src/modules/action.cpp index ecdcdbc..a70ae3f 100644 --- a/src/modules/action.cpp +++ b/src/modules/action.cpp @@ -46,24 +46,20 @@ bool binding_is_down(const asw::input::ActionBinding& binding, float& out_streng return false; } else if constexpr (std::is_same_v) { - if (b.controller_index < asw::input::controller.size() - && asw::input::controller[b.controller_index] - .down[static_cast(b.button)]) { + if (asw::input::get_controller_button_down(b.controller_index, b.button)) { out_strength = std::max(out_strength, 1.0F); return true; } return false; } else if constexpr (std::is_same_v) { - if (b.controller_index < asw::input::controller.size()) { - const float val - = asw::input::controller[b.controller_index].axis[static_cast(b.axis)]; - const float effective = b.positive_direction ? val : -val; - if (effective >= b.threshold) { - out_strength = std::max(out_strength, effective); - return true; - } + const float val = asw::input::get_controller_axis(b.controller_index, b.axis); + const float effective = b.positive_direction ? val : -val; + if (effective >= b.threshold) { + out_strength = std::max(out_strength, effective); + return true; } + return false; } @@ -87,9 +83,7 @@ bool binding_is_pressed(const asw::input::ActionBinding& binding) return asw::input::mouse.pressed[static_cast(b.button)]; } else if constexpr (std::is_same_v) { - return b.controller_index < asw::input::controller.size() - && asw::input::controller[b.controller_index] - .pressed[static_cast(b.button)]; + return asw::input::get_controller_button_down(b.controller_index, b.button); } else { // ControllerAxisBinding: transition handled by prev_down in update_actions. @@ -113,9 +107,7 @@ bool binding_is_released(const asw::input::ActionBinding& binding) return asw::input::mouse.released[static_cast(b.button)]; } else if constexpr (std::is_same_v) { - return b.controller_index < asw::input::controller.size() - && asw::input::controller[b.controller_index] - .released[static_cast(b.button)]; + return asw::input::get_controller_button_up(b.controller_index, b.button); } else { // ControllerAxisBinding: transition handled by prev_down in update_actions. diff --git a/src/modules/assets.cpp b/src/modules/assets.cpp index 03076fd..71996f4 100644 --- a/src/modules/assets.cpp +++ b/src/modules/assets.cpp @@ -8,6 +8,7 @@ #include #include "./asw/modules/display.h" +#include "./asw/modules/sound.h" #include "./asw/modules/types.h" #include "./asw/modules/util.h" @@ -129,13 +130,13 @@ void asw::assets::unload_font(const std::string& key) asw::Sample asw::assets::load_sample(const std::string& filename) { const auto full_path = get_path(filename); - Mix_Chunk* temp = Mix_LoadWAV(full_path.c_str()); + MIX_Audio* temp = MIX_LoadAudio(asw::sound::mixer, full_path.c_str(), true); if (temp == nullptr) { asw::util::abort_on_error("Failed to load sample: " + full_path); } - return { temp, Mix_FreeChunk }; + return { temp, MIX_DestroyAudio }; } asw::Sample asw::assets::load_sample(const std::string& filename, const std::string& key) @@ -168,13 +169,13 @@ void asw::assets::unload_sample(const std::string& key) asw::Music asw::assets::load_music(const std::string& filename) { const auto full_path = get_path(filename); - Mix_Music* temp = Mix_LoadMUS(full_path.c_str()); + MIX_Audio* temp = MIX_LoadAudio(asw::sound::mixer, full_path.c_str(), false); if (temp == nullptr) { asw::util::abort_on_error("Failed to load music: " + full_path); } - return { temp, Mix_FreeMusic }; + return { temp, MIX_DestroyAudio }; } asw::Music asw::assets::load_music(const std::string& filename, const std::string& key) diff --git a/src/modules/core.cpp b/src/modules/core.cpp index ff79124..5ed6bdf 100644 --- a/src/modules/core.cpp +++ b/src/modules/core.cpp @@ -9,17 +9,23 @@ #include "./asw/modules/display.h" #include "./asw/modules/input.h" #include "./asw/modules/log.h" +#include "./asw/modules/sound.h" #include "./asw/modules/util.h" -bool asw::core::exit = false; +namespace { +bool exiting = false; +} void asw::core::update() { + if (exiting) { + return; + } + asw::input::reset(); auto& mouse = asw::input::mouse; auto& keyboard = asw::input::keyboard; - auto& controller = asw::input::controller; SDL_Event e; @@ -105,67 +111,27 @@ void asw::core::update() } case SDL_EVENT_GAMEPAD_AXIS_MOTION: { - if (e.gaxis.which >= asw::input::MAX_CONTROLLERS) { - break; - } - - auto motion = static_cast(e.gaxis.value) / 32768.0F; - - if (auto& current = controller[e.gaxis.which]; motion > current.dead_zone) { - current.axis[e.gaxis.axis] = motion; - } else if (motion < -current.dead_zone) { - current.axis[e.gaxis.axis] = motion; - } else { - current.axis[e.gaxis.axis] = 0.0F; - } - + asw::input::_controller_axis_motion(e.gaxis.which, e.gaxis.axis, e.gaxis.value); break; } case SDL_EVENT_GAMEPAD_BUTTON_DOWN: { - if (e.gbutton.which >= asw::input::MAX_CONTROLLERS) { - break; - } - - auto button = static_cast(e.gbutton.button); - controller[e.gbutton.which].pressed[button] = true; - controller[e.gbutton.which].down[button] = true; - controller[e.gbutton.which].any_pressed = true; - controller[e.gbutton.which].last_pressed = button; + asw::input::_controller_button_down(e.gbutton.which, e.gbutton.button); break; } case SDL_EVENT_GAMEPAD_BUTTON_UP: { - if (e.gbutton.which >= asw::input::MAX_CONTROLLERS) { - break; - } - - auto button = static_cast(e.gbutton.button); - controller[e.gbutton.which].released[button] = true; - controller[e.gbutton.which].down[button] = false; + asw::input::_controller_button_up(e.gbutton.which, e.gbutton.button); break; } case SDL_EVENT_GAMEPAD_ADDED: { - if (e.gdevice.which >= asw::input::MAX_CONTROLLERS || !SDL_IsGamepad(e.gdevice.which)) { - asw::log::warn(std::format("Failed to open gamepad: {}", e.gdevice.which)); - break; - } - - SDL_OpenGamepad(e.gdevice.which); - + asw::input::_controller_added(e.gdevice.which); break; } case SDL_EVENT_GAMEPAD_REMOVED: { - if (e.gdevice.which >= asw::input::MAX_CONTROLLERS) { - break; - } - - if (auto* existing = SDL_GetGamepadFromID(e.gdevice.which); existing != nullptr) { - SDL_CloseGamepad(existing); - } - + asw::input::_controller_removed(e.gdevice.which); break; } @@ -175,7 +141,7 @@ void asw::core::update() } case SDL_EVENT_QUIT: { - exit = true; + exit(); break; } @@ -187,7 +153,7 @@ void asw::core::update() void asw::core::init(int width, int height, int scale) { - if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_GAMEPAD)) { + if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_GAMEPAD)) { asw::util::abort_on_error("SDL_Init"); } @@ -195,19 +161,12 @@ void asw::core::init(int width, int height, int scale) asw::util::abort_on_error("TTF_Init"); } - // Initialize SDL_mixer - SDL_AudioSpec spec; - spec.format = SDL_AUDIO_S16LE; - spec.freq = 44100; - spec.channels = 2; - - if (!Mix_OpenAudio(0, &spec)) { - asw::util::abort_on_error("Mix_OpenAudio"); + if (!asw::sound::_init()) { + asw::util::abort_on_error("Sound initialization failed"); } asw::display::window = SDL_CreateWindow("", width * scale, height * scale, SDL_WINDOW_RESIZABLE); - if (asw::display::window == nullptr) { asw::util::abort_on_error("WINDOW"); } @@ -232,9 +191,20 @@ void asw::core::print_info() renderer_name = SDL_GetRendererName(asw::display::renderer); } - const std::string sdl_version - = std::format("{}.{}.{}", SDL_MAJOR_VERSION, SDL_MINOR_VERSION, SDL_MICRO_VERSION); + asw::log::info( + "SDL Version: {}.{}.{}", SDL_MAJOR_VERSION, SDL_MINOR_VERSION, SDL_MICRO_VERSION); + asw::log::info("Renderer: {}", renderer_name); +} - asw::log::info(std::format("SDL Version: {}", sdl_version)); - asw::log::info(std::format("Renderer: {}", renderer_name)); +void asw::core::exit() +{ + exiting = true; + MIX_Quit(); + TTF_Quit(); + SDL_Quit(); } + +bool asw::core::is_exiting() +{ + return exiting; +} \ No newline at end of file diff --git a/src/modules/input.cpp b/src/modules/input.cpp index b89edc9..2b8f732 100644 --- a/src/modules/input.cpp +++ b/src/modules/input.cpp @@ -1,19 +1,27 @@ #include "./asw/modules/input.h" +#include +#include + #include "./asw/modules/action.h" +#include "./asw/modules/log.h" namespace { /// @brief Active cursor stores the current active cursor. It is updated by /// the core. std::array cursors { nullptr }; + +/// @brief Global controller state. +std::vector controller {}; + +/// @brief Map of SDL_JoystickID to controller index in the controller vector. +std::unordered_map controller_id_map {}; } // namespace asw::input::KeyState asw::input::keyboard {}; asw::input::MouseState asw::input::mouse {}; -std::array asw::input::controller {}; - std::string asw::input::text_input; void asw::input::reset() @@ -23,7 +31,6 @@ void asw::input::reset() auto& k_state = asw::input::keyboard; auto& m_state = asw::input::mouse; - auto& c_state = asw::input::controller; // Clear key state k_state.any_pressed = false; @@ -54,7 +61,7 @@ void asw::input::reset() asw::input::text_input.clear(); // Clear controller state - for (auto& cont : c_state) { + for (auto& cont : controller) { cont.any_pressed = false; cont.last_pressed = -1; @@ -68,6 +75,9 @@ void asw::input::reset() } } +// ---- MOUSE ---- +// + bool asw::input::get_mouse_button(asw::input::MouseButton button) { return mouse.down[static_cast(button)]; @@ -83,6 +93,24 @@ bool asw::input::get_mouse_button_up(asw::input::MouseButton button) return mouse.released[static_cast(button)]; } +void asw::input::set_cursor(asw::input::CursorId cursor) +{ + auto cursor_int = static_cast(cursor); + + if (cursor_int >= cursors.size()) { + return; + } + + if (cursors[cursor_int] == nullptr) { + cursors[cursor_int] = SDL_CreateSystemCursor(static_cast(cursor_int)); + } + + SDL_SetCursor(cursors[cursor_int]); +} + +// ---- KEYBOARD ---- +// + bool asw::input::get_key(asw::input::Key key) { return keyboard.down[static_cast(key)]; @@ -98,19 +126,95 @@ bool asw::input::get_key_up(asw::input::Key key) return keyboard.released[static_cast(key)]; } -void asw::input::set_cursor(asw::input::CursorId cursor) +// ---- CONTROLLER ---- +// + +void asw::input::_controller_added(SDL_JoystickID id) { - auto cursor_int = static_cast(cursor); + if (!SDL_IsGamepad(id)) { + asw::log::warn("Failed to open gamepad: {}", id); + return; + } - if (cursor_int >= cursors.size()) { + auto* opened = SDL_OpenGamepad(id); + if (opened == nullptr) { + asw::log::warn("Failed to open gamepad: {}", id); + } + + // Add controller + auto& new_controller = controller.emplace_back(); + new_controller.gamepad = opened; + new_controller.name = SDL_GetGamepadName(opened); + controller_id_map[id] = controller.size() - 1; + + asw::log::info("Gamepad added: {} (ID: {})", new_controller.name, id); +} + +void asw::input::_controller_removed(SDL_JoystickID id) +{ + const auto it = controller_id_map.find(id); + if (it == controller_id_map.end()) { return; } - if (cursors[cursor_int] == nullptr) { - cursors[cursor_int] = SDL_CreateSystemCursor(static_cast(cursor_int)); + // Erase from vector and map + const auto index = it->second; + controller_id_map.erase(it); + controller.erase(controller.begin() + index); + + // Close gamepad if it exists + if (auto* existing = SDL_GetGamepadFromID(id); existing != nullptr) { + SDL_CloseGamepad(existing); } +} - SDL_SetCursor(cursors[cursor_int]); +void asw::input::_controller_axis_motion(SDL_JoystickID id, uint32_t axis, float value) +{ + const auto it = controller_id_map.find(id); + if (it == controller_id_map.end()) { + return; + } + + const auto index = it->second; + if (index >= controller.size() || axis >= asw::input::NUM_CONTROLLER_AXES) { + return; + } + + controller[index].axis[axis] = value / 32768.0F; // Normalize to [-1, 1] +} + +void asw::input::_controller_button_down(SDL_JoystickID id, uint32_t button) +{ + const auto it = controller_id_map.find(id); + if (it == controller_id_map.end()) { + return; + } + + const auto index = it->second; + if (index >= controller.size()) { + return; + } + + controller[index].pressed[button] = true; + controller[index].down[button] = true; + controller[index].any_pressed = true; + controller[index].last_pressed = button; +} + +void asw::input::_controller_button_up(SDL_JoystickID id, uint32_t button) +{ + const auto it = controller_id_map.find(id); + if (it == controller_id_map.end()) { + return; + } + + const auto index = it->second; + if (index >= controller.size()) { + return; + } + + controller[index].released[button] = true; + controller[index].down[button] = false; } bool asw::input::get_controller_button(uint32_t index, asw::input::ControllerButton button) @@ -160,9 +264,7 @@ void asw::input::set_controller_dead_zone(uint32_t index, float dead_zone) int asw::input::get_controller_count() { - int count = 0; - SDL_GetJoysticks(&count); - return count; + return controller.size(); } std::string asw::input::get_controller_name(uint32_t index) @@ -171,5 +273,5 @@ std::string asw::input::get_controller_name(uint32_t index) return ""; } - return SDL_GetGamepadNameForID(index); + return controller.at(index).name; } \ No newline at end of file diff --git a/src/modules/sound.cpp b/src/modules/sound.cpp index 0e2da04..3e4e628 100644 --- a/src/modules/sound.cpp +++ b/src/modules/sound.cpp @@ -1,85 +1,156 @@ #include "./asw/modules/sound.h" +#include #include +#include +#include -#include +#include "./asw/modules/log.h" namespace { float master_volume = 1.0F; float sfx_volume = 1.0F; float music_volume = 1.0F; -int compute_sfx_volume(float vol) +std::array tracks; +MIX_Track* music_track = nullptr; + +float compute_sfx_volume(float vol) { - auto volume = vol * 255.0F * sfx_volume * master_volume; - return static_cast(volume); + auto volume = vol * sfx_volume; + return std::clamp(volume, 0.0F, 1.0F); } -int compute_music_volume(float vol) +float compute_music_volume(float vol) { - auto volume = vol * 255.0F * music_volume * master_volume; - return static_cast(volume); + auto volume = vol * music_volume; + return std::clamp(volume, 0.0F, 1.0F); +} + +int find_free_track() +{ + for (size_t i = 0; i < tracks.size(); ++i) { + if (!MIX_TrackPlaying(tracks[i])) { + return static_cast(i); + } + } + return -1; } + } // namespace -void asw::sound::play(const asw::Sample& sample, float volume, float pan, bool loop) +MIX_Mixer* asw::sound::mixer = nullptr; + +bool asw::sound::_init() { - const int channel = Mix_PlayChannel(-1, sample.get(), loop ? -1 : 0); - if (channel >= 0) { - Mix_Volume(channel, compute_sfx_volume(volume)); + if (!MIX_Init()) { + asw::log::error("Failed to initialize SDL_mixer: {}", SDL_GetError()); + return false; + } - auto panning = (std::clamp(pan, -1.0F, 1.0F) + 1.0F) * 127.5F; - auto int_pan = static_cast(panning); + if (mixer != nullptr) { + asw::log::warn("Mixer already initialized"); + return true; + } - Mix_SetPanning(channel, 255 - int_pan, int_pan); + // Initialize SDL_mixer + SDL_AudioSpec spec; + spec.format = SDL_AUDIO_S16LE; + spec.freq = 44100; + spec.channels = 2; + + mixer = MIX_CreateMixerDevice(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &spec); + if (mixer == nullptr) { + asw::log::error("Failed to create mixer: {}", SDL_GetError()); + return false; } -} -void asw::sound::play_music(const asw::Music& sample, float volume) -{ - Mix_VolumeMusic(compute_music_volume(volume)); - Mix_PlayMusic(sample.get(), -1); + for (auto& track : tracks) { + track = MIX_CreateTrack(mixer); + if (track == nullptr) { + asw::log::error("Failed to create track: {}", SDL_GetError()); + return false; + } + } + + music_track = MIX_CreateTrack(mixer); + if (music_track == nullptr) { + asw::log::error("Failed to create music track: {}", SDL_GetError()); + return false; + } + + return true; } -void asw::sound::stop_music() +void asw::sound::play(const asw::Sample& sample, float volume, float pan, bool loop) { - Mix_HaltMusic(); + const int channel = find_free_track(); + if (channel >= 0) { + // Find first free track and play sample on it + auto& track = tracks[channel]; + MIX_SetTrackAudio(track, sample.get()); + MIX_SetTrackGain(track, compute_sfx_volume(volume)); + + // Stereo gains for panning using equal power panning + const float left = std::sqrtf((1.0F - pan) * 0.5F); + const float right = std::sqrtf((1.0F + pan) * 0.5F); + + MIX_StereoGains gains; + gains.left = left; + gains.right = right; + MIX_SetTrackStereo(track, &gains); + + // Play the track, looping if requested + const SDL_PropertiesID options = SDL_CreateProperties(); + SDL_SetNumberProperty(options, MIX_PROP_PLAY_LOOPS_NUMBER, loop ? -1 : 0); + MIX_PlayTrack(track, options); + } } -void asw::sound::fade_in_music(const asw::Music& music, float volume, float duration) +void asw::sound::play_music(const asw::Music& sample, float volume, float fade_in_s) { - Mix_FadeInMusic(music.get(), -1, static_cast(duration * 1000.0F)); - Mix_VolumeMusic(compute_music_volume(volume)); + MIX_SetTrackGain(music_track, compute_music_volume(volume)); + MIX_SetTrackAudio(music_track, sample.get()); + + const SDL_PropertiesID options = SDL_CreateProperties(); + SDL_SetNumberProperty(options, MIX_PROP_PLAY_LOOPS_NUMBER, -1); + SDL_SetNumberProperty( + options, MIX_PROP_PLAY_FADE_IN_MILLISECONDS_NUMBER, static_cast(fade_in_s * 1000.0F)); + MIX_PlayTrack(music_track, options); } -void asw::sound::fade_out_music(float duration) +void asw::sound::stop_music(float fade_out_s) + { - Mix_FadeOutMusic(static_cast(duration * 1000.0F)); + const auto fade_out_frames + = MIX_TrackMSToFrames(music_track, static_cast(fade_out_s * 1000.0F)); + MIX_StopTrack(music_track, fade_out_frames); } void asw::sound::pause_music() { - Mix_PauseMusic(); + MIX_PauseTrack(music_track); } void asw::sound::resume_music() { - Mix_ResumeMusic(); + MIX_ResumeTrack(music_track); } bool asw::sound::is_music_playing() { - return Mix_PlayingMusic(); + return MIX_TrackPlaying(music_track); } bool asw::sound::is_music_paused() { - return Mix_PausedMusic(); + return MIX_TrackPaused(music_track); } void asw::sound::set_master_volume(float volume) { master_volume = std::clamp(volume, 0.0F, 1.0F); + MIX_SetMixerGain(mixer, master_volume); } void asw::sound::set_sfx_volume(float volume) From 1cbc5b768ea07975e75e511c78784b030fdbfdcc Mon Sep 17 00:00:00 2001 From: Allan Legemaate Date: Tue, 17 Mar 2026 23:28:51 -0400 Subject: [PATCH 6/8] feat: type aliases --- include/asw/modules/geometry.h | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/include/asw/modules/geometry.h b/include/asw/modules/geometry.h index 608f391..0a84f4f 100644 --- a/include/asw/modules/geometry.h +++ b/include/asw/modules/geometry.h @@ -555,6 +555,14 @@ template class Quad { Vec2 size; }; +/// Type aliases for common vector and rectangle types. +using Vec2f = Vec2; +using Vec2i = Vec2; +using Vec3f = Vec3; +using Vec3i = Vec3; +using Quadf = Quad; +using Quadi = Quad; + } // namespace asw #endif // ASW_GEOMETRY_H \ No newline at end of file From 7d845d4dff0deb915a8336efc83c8c0c32e2d66e Mon Sep 17 00:00:00 2001 From: Allan Legemaate Date: Wed, 18 Mar 2026 00:19:01 -0400 Subject: [PATCH 7/8] fix: exiting cases --- src/modules/core.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/modules/core.cpp b/src/modules/core.cpp index 5ed6bdf..baab6bc 100644 --- a/src/modules/core.cpp +++ b/src/modules/core.cpp @@ -148,6 +148,10 @@ void asw::core::update() default: break; } + + if (exiting) { + break; + } } } @@ -199,6 +203,10 @@ void asw::core::print_info() void asw::core::exit() { exiting = true; + SDL_DestroyRenderer(asw::display::renderer); + asw::display::renderer = nullptr; + SDL_DestroyWindow(asw::display::window); + asw::display::window = nullptr; MIX_Quit(); TTF_Quit(); SDL_Quit(); From b18ad12b3496394c4fbb98ad92100333b7e7e636 Mon Sep 17 00:00:00 2001 From: Allan Legemaate Date: Wed, 18 Mar 2026 00:30:27 -0400 Subject: [PATCH 8/8] chore: separate exit from cleanup --- examples/actions/main.cpp | 4 ++-- examples/controller/main.cpp | 3 ++- examples/keyboard/main.cpp | 3 ++- examples/mouse/main.cpp | 3 ++- examples/primitives/main.cpp | 3 ++- include/asw/modules/core.h | 6 +++++- src/modules/core.cpp | 28 +++++++++++++++------------- 7 files changed, 30 insertions(+), 20 deletions(-) diff --git a/examples/actions/main.cpp b/examples/actions/main.cpp index 3927920..52ecb54 100644 --- a/examples/actions/main.cpp +++ b/examples/actions/main.cpp @@ -67,7 +67,6 @@ int main() if (asw::input::is_action_pressed("quit")) { asw::core::exit(); - break; } // Move using analogue strength so a controller stick gives smooth speed @@ -143,6 +142,7 @@ int main() asw::display::present(); } - asw::input::clear_actions(); + asw::core::cleanup(); + return 0; } diff --git a/examples/controller/main.cpp b/examples/controller/main.cpp index 4b864ad..e5ec765 100644 --- a/examples/controller/main.cpp +++ b/examples/controller/main.cpp @@ -70,7 +70,6 @@ int main() if (asw::input::get_key_down(asw::input::Key::Escape)) { asw::core::exit(); - break; } // Log button events for controller 0 @@ -160,5 +159,7 @@ int main() asw::display::present(); } + asw::core::cleanup(); + return 0; } diff --git a/examples/keyboard/main.cpp b/examples/keyboard/main.cpp index 5d6391c..9132e33 100644 --- a/examples/keyboard/main.cpp +++ b/examples/keyboard/main.cpp @@ -65,7 +65,6 @@ int main() } if (asw::input::get_key_down(asw::input::Key::Escape)) { asw::core::exit(); - break; } // --- Text input --- @@ -100,5 +99,7 @@ int main() asw::display::present(); } + asw::core::cleanup(); + return 0; } diff --git a/examples/mouse/main.cpp b/examples/mouse/main.cpp index f7c1573..888eab0 100644 --- a/examples/mouse/main.cpp +++ b/examples/mouse/main.cpp @@ -62,7 +62,6 @@ int main() if (asw::input::get_key_down(asw::input::Key::Escape)) { asw::core::exit(); - break; } // --- Draw --- @@ -104,5 +103,7 @@ int main() asw::display::present(); } + asw::core::cleanup(); + return 0; } diff --git a/examples/primitives/main.cpp b/examples/primitives/main.cpp index e622a78..84dffe1 100644 --- a/examples/primitives/main.cpp +++ b/examples/primitives/main.cpp @@ -29,7 +29,6 @@ int main() if (asw::input::get_key_down(asw::input::Key::Escape)) { asw::core::exit(); - break; } angle += 0.02F; @@ -108,5 +107,7 @@ int main() asw::display::present(); } + asw::core::cleanup(); + return 0; } diff --git a/include/asw/modules/core.h b/include/asw/modules/core.h index e5e6bf4..70c9dab 100644 --- a/include/asw/modules/core.h +++ b/include/asw/modules/core.h @@ -28,7 +28,7 @@ void init(int width, int height, int scale = 1); void print_info(); /// @brief Exit the application. -/// Calls SDL_Quit() and performs any necessary cleanup. +/// Sets exiting flag to true, which will cause the main loop to exit on the next update. /// void exit(); @@ -36,6 +36,10 @@ void exit(); /// bool is_exiting(); +/// @brief Cleanup resources used by the core module. Should be called on application exit. +/// +void cleanup(); + } // namespace asw::core #endif // ASW_CORE_H \ No newline at end of file diff --git a/src/modules/core.cpp b/src/modules/core.cpp index baab6bc..e7b20db 100644 --- a/src/modules/core.cpp +++ b/src/modules/core.cpp @@ -6,6 +6,7 @@ #include #include +#include "./asw/modules/action.h" #include "./asw/modules/display.h" #include "./asw/modules/input.h" #include "./asw/modules/log.h" @@ -18,10 +19,6 @@ bool exiting = false; void asw::core::update() { - if (exiting) { - return; - } - asw::input::reset(); auto& mouse = asw::input::mouse; @@ -142,16 +139,11 @@ void asw::core::update() case SDL_EVENT_QUIT: { exit(); - break; } default: break; } - - if (exiting) { - break; - } } } @@ -203,10 +195,20 @@ void asw::core::print_info() void asw::core::exit() { exiting = true; - SDL_DestroyRenderer(asw::display::renderer); - asw::display::renderer = nullptr; - SDL_DestroyWindow(asw::display::window); - asw::display::window = nullptr; +} + +void asw::core::cleanup() +{ + asw::input::clear_actions(); + + if (asw::display::renderer != nullptr) { + SDL_DestroyRenderer(asw::display::renderer); + asw::display::renderer = nullptr; + } + if (asw::display::window != nullptr) { + SDL_DestroyWindow(asw::display::window); + asw::display::window = nullptr; + } MIX_Quit(); TTF_Quit(); SDL_Quit();