From 307e07d6513f9524b7fcab5134494bd175465cd1 Mon Sep 17 00:00:00 2001 From: Hannes Schulze Date: Thu, 30 Jan 2025 16:43:06 +0100 Subject: handle input devices in the c++ application --- lib/nes/CMakeLists.txt | 3 +- lib/nes/app/CMakeLists.txt | 7 ++ lib/nes/app/application.cc | 38 ++++++++ lib/nes/app/application.hh | 37 ++++++++ lib/nes/app/input/CMakeLists.txt | 8 ++ lib/nes/app/input/input-device-controller.hh | 38 ++++++++ lib/nes/app/input/input-device-keyboard.hh | 16 ++++ lib/nes/app/input/input-device.hh | 44 +++++++++ lib/nes/app/input/input-manager.cc | 102 +++++++++++++++++++++ lib/nes/app/input/input-manager.hh | 45 ++++++++++ lib/nes/common/CMakeLists.txt | 1 + lib/nes/common/list.hh | 130 +++++++++++++++++++++++++++ 12 files changed, 468 insertions(+), 1 deletion(-) create mode 100644 lib/nes/app/CMakeLists.txt create mode 100644 lib/nes/app/application.cc create mode 100644 lib/nes/app/application.hh create mode 100644 lib/nes/app/input/CMakeLists.txt create mode 100644 lib/nes/app/input/input-device-controller.hh create mode 100644 lib/nes/app/input/input-device-keyboard.hh create mode 100644 lib/nes/app/input/input-device.hh create mode 100644 lib/nes/app/input/input-manager.cc create mode 100644 lib/nes/app/input/input-manager.hh create mode 100644 lib/nes/common/list.hh (limited to 'lib') diff --git a/lib/nes/CMakeLists.txt b/lib/nes/CMakeLists.txt index 78c09fe..f24c27f 100644 --- a/lib/nes/CMakeLists.txt +++ b/lib/nes/CMakeLists.txt @@ -1,2 +1,3 @@ add_subdirectory(common) -add_subdirectory(sys) \ No newline at end of file +add_subdirectory(sys) +add_subdirectory(app) \ No newline at end of file diff --git a/lib/nes/app/CMakeLists.txt b/lib/nes/app/CMakeLists.txt new file mode 100644 index 0000000..18b2a2c --- /dev/null +++ b/lib/nes/app/CMakeLists.txt @@ -0,0 +1,7 @@ +target_sources( + nes + PRIVATE + application.hh + application.cc) + +add_subdirectory(input) \ No newline at end of file diff --git a/lib/nes/app/application.cc b/lib/nes/app/application.cc new file mode 100644 index 0000000..db43854 --- /dev/null +++ b/lib/nes/app/application.cc @@ -0,0 +1,38 @@ +#include "nes/app/application.hh" +#include "nes/app/input/input-device-keyboard.hh" +#include "nes/app/input/input-device-controller.hh" +#include + +namespace nes +{ + namespace + { + constexpr auto file = std::string_view{ "/Users/hannes/Documents/temp/supermario.nes" }; + } // namespace + + application::application(display& display, input_device_keyboard& keyboard) + : display_{ display } + , input_manager_{ keyboard } + , console_{ cartridge::from_file(file), display_ } + { + if (console_.get_status() != status::success) + { + std::cerr << "Unable to load cartridge: " << to_string(console_.get_status()) << std::endl; + std::abort(); + } + } + + auto application::frame(std::chrono::microseconds const elapsed_time) -> void + { + console_.ref_controller_1().set_pressed(input_manager_.get_input_1().read_mask()); + console_.ref_controller_2().set_pressed(input_manager_.get_input_2().read_mask()); + + console_.step(elapsed_time); + + if (console_.get_status() != status::success) + { + std::cerr << "Invalid state: " << to_string(console_.get_status()) << std::endl; + std::abort(); + } + } +} // namespace nes \ No newline at end of file diff --git a/lib/nes/app/application.hh b/lib/nes/app/application.hh new file mode 100644 index 0000000..9f6bd16 --- /dev/null +++ b/lib/nes/app/application.hh @@ -0,0 +1,37 @@ +#pragma once + +#include "nes/app/input/input-manager.hh" +#include "nes/sys/nes.hh" +#include + +namespace nes +{ + class display; + + /// Entrypoint for the application. + /// + /// This class takes care of presenting a UI screen or the emulator, handling any errors and implementing the main + /// application logic. + /// + /// It delegates some platform-specific tasks to interfaces. + class application + { + display& display_; + input_manager input_manager_; + nes console_; + + public: + explicit application(display&, input_device_keyboard&); + + application(application const&) = delete; + application(application&&) = delete; + auto operator=(application const&) -> application& = delete; + auto operator=(application&&) -> application& = delete; + + /// Request a new frame for the application. + auto frame(std::chrono::microseconds elapsed_time) -> void; + + auto add_controller(input_device_controller& c) -> void { input_manager_.add_controller(c); } + auto remove_controller(input_device_controller& c) -> void { input_manager_.remove_controller(c); } + }; +} // namespace nes \ No newline at end of file diff --git a/lib/nes/app/input/CMakeLists.txt b/lib/nes/app/input/CMakeLists.txt new file mode 100644 index 0000000..7150745 --- /dev/null +++ b/lib/nes/app/input/CMakeLists.txt @@ -0,0 +1,8 @@ +target_sources( + nes + PRIVATE + input-device.hh + input-device-keyboard.hh + input-device-controller.hh + input-manager.hh + input-manager.cc) \ No newline at end of file diff --git a/lib/nes/app/input/input-device-controller.hh b/lib/nes/app/input/input-device-controller.hh new file mode 100644 index 0000000..6adb79c --- /dev/null +++ b/lib/nes/app/input/input-device-controller.hh @@ -0,0 +1,38 @@ +#pragma once + +#include "nes/app/input/input-device.hh" +#include "nes/common/list.hh" +#include + +namespace nes +{ + /// A game controller input device. + class input_device_controller : public input_device, public list::node + { + public: + enum class index + { + unused = -1, + player_1 = 0, + player_2 = 1, + }; + + /// Indicates whether the controller can be trusted to actually exist. + /// + /// If this returns false, the controller is not automatically selected on startup if there is no other + /// controller already selected. + virtual auto is_reliable() const -> bool = 0; + + /// Human-readable name for the controller. + virtual auto get_name() const -> std::string_view = 0; + + /// Get the (persistent) player index for the controller, if already set before. + virtual auto get_index() const -> index = 0; + + /// Set the (persistent) player index for the controller. + virtual auto set_index(index) -> void = 0; + + protected: + explicit input_device_controller() = default; + }; +} // namespace nes \ No newline at end of file diff --git a/lib/nes/app/input/input-device-keyboard.hh b/lib/nes/app/input/input-device-keyboard.hh new file mode 100644 index 0000000..d61417d --- /dev/null +++ b/lib/nes/app/input/input-device-keyboard.hh @@ -0,0 +1,16 @@ +#pragma once + +#include "nes/app/input/input-device.hh" + +namespace nes +{ + /// A keyboard input device. + class input_device_keyboard : public input_device + { + public: + virtual auto read_key_escape() -> bool = 0; + + protected: + explicit input_device_keyboard() = default; + }; +} // namespace nes \ No newline at end of file diff --git a/lib/nes/app/input/input-device.hh b/lib/nes/app/input/input-device.hh new file mode 100644 index 0000000..9ac7423 --- /dev/null +++ b/lib/nes/app/input/input-device.hh @@ -0,0 +1,44 @@ +#pragma once + +#include "nes/sys/types/button-mask.hh" + +namespace nes +{ + /// An abstract input device which can be used to play games. + class input_device + { + public: + virtual ~input_device() = default; + + input_device(input_device const&) = delete; + input_device(input_device&&) = delete; + auto operator=(input_device const&) -> input_device& = delete; + auto operator=(input_device&&) -> input_device& = delete; + + virtual auto read_key_up() -> bool = 0; + virtual auto read_key_down() -> bool = 0; + virtual auto read_key_left() -> bool = 0; + virtual auto read_key_right() -> bool = 0; + virtual auto read_key_a() -> bool = 0; + virtual auto read_key_b() -> bool = 0; + virtual auto read_key_select() -> bool = 0; + virtual auto read_key_start() -> bool = 0; + + auto read_mask() -> button_mask + { + auto res = button_mask{}; + if (read_key_up()) { res.add(buttons::up); } + if (read_key_down()) { res.add(buttons::down); } + if (read_key_left()) { res.add(buttons::left); } + if (read_key_right()) { res.add(buttons::right); } + if (read_key_a()) { res.add(buttons::a); } + if (read_key_b()) { res.add(buttons::b); } + if (read_key_select()) { res.add(buttons::select); } + if (read_key_start()) { res.add(buttons::start); } + return res; + } + + protected: + explicit input_device() = default; + }; +} // namespace nes \ No newline at end of file diff --git a/lib/nes/app/input/input-manager.cc b/lib/nes/app/input/input-manager.cc new file mode 100644 index 0000000..1c9db9e --- /dev/null +++ b/lib/nes/app/input/input-manager.cc @@ -0,0 +1,102 @@ +#include "nes/app/input/input-manager.hh" + +namespace nes +{ + namespace + { + class input_device_dummy final : public input_device + { + public: + explicit input_device_dummy() = default; + + static auto get_instance() -> input_device_dummy& + { + static auto res = input_device_dummy{}; + return res; + } + + auto read_key_up() -> bool override { return false; } + auto read_key_down() -> bool override { return false; } + auto read_key_left() -> bool override { return false; } + auto read_key_right() -> bool override { return false; } + auto read_key_a() -> bool override { return false; } + auto read_key_b() -> bool override { return false; } + auto read_key_select() -> bool override { return false; } + auto read_key_start() -> bool override { return false; } + }; + } // namespace + + input_manager::input_manager(input_device_keyboard& keyboard) + : keyboard_{ &keyboard } + { + fill_unassigned_inputs(); + } + + auto input_manager::add_controller(input_device_controller& controller) -> void + { + controllers_.append(controller); + if (controller.is_reliable()) + { + auto const index = controller.get_index(); + if (index != input_device_controller::index::unused) + { + // This controller was previously used and assigned an index -> replace the current device with this + // (known) controller. + auto const i = static_cast(index); + if (inputs_[i].controller) + { + inputs_[i].controller->set_index(input_device_controller::index::unused); + } + + inputs_[i].controller = &controller; + inputs_[i].device = &controller; + } + + // There may be another free controller now. + fill_unassigned_inputs(); + } + } + + auto input_manager::remove_controller(input_device_controller& controller) -> void + { + controllers_.remove(controller); + fill_unassigned_inputs(); + } + + auto input_manager::fill_unassigned_inputs() -> void + { + auto it = controllers_.begin(); + input_device* fallback = keyboard_; + + for (auto i = std::size_t{ 0 }; i < input_count; ++i) + { + if (inputs_[i].controller != nullptr) { continue; } + + // This input is not assigned -> use the next available controller if available. + while (it != controllers_.end()) + { + auto controller = *it; + if (controller->is_reliable() && controller->get_index() == input_device_controller::index::unused) + { + break; + } + ++it; + } + + if (it != controllers_.end()) + { + // We've found an unused controller -> assign it. + inputs_[i].controller = *it; + inputs_[i].controller->set_index(static_cast(i)); + inputs_[i].device = *it; + } + else + { + // We're all out of controllers -> fall back to the keyboard (if this is the first missing controller) + // or nothing. + inputs_[i].device = fallback; + fallback = &input_device_dummy::get_instance(); // Only use the keyboard as a fallback once. + } + } + } +} // namespace nes \ No newline at end of file diff --git a/lib/nes/app/input/input-manager.hh b/lib/nes/app/input/input-manager.hh new file mode 100644 index 0000000..10308c7 --- /dev/null +++ b/lib/nes/app/input/input-manager.hh @@ -0,0 +1,45 @@ +#pragma once + +#include "nes/app/input/input-device-keyboard.hh" +#include "nes/app/input/input-device-controller.hh" +#include "nes/common/list.hh" + +namespace nes +{ + /// Manages input devices. + /// + /// The main purpose of this class is to assign newly plugged in controllers as the primary/secondary input and fall + /// back to the keyboard if needed. + class input_manager + { + static constexpr auto input_count = std::size_t{ 2 }; + + input_device_keyboard* keyboard_; + list controllers_; + struct + { + input_device* device{ nullptr }; + input_device_controller* controller{ nullptr }; + } inputs_[input_count]{}; + + public: + explicit input_manager(input_device_keyboard&); + + input_manager(input_manager const&) = delete; + input_manager(input_manager&&) = delete; + auto operator=(input_manager const&) -> input_manager& = delete; + auto operator=(input_manager&&) -> input_manager& = delete; + + auto get_input_1() const -> input_device& { return *inputs_[0].device; } + auto get_input_2() const -> input_device& { return *inputs_[1].device; } + + auto get_keyboard() const -> input_device_keyboard& { return *keyboard_; } + auto get_controllers() const -> list const& { return controllers_; } + + auto add_controller(input_device_controller&) -> void; + auto remove_controller(input_device_controller&) -> void; + + private: + auto fill_unassigned_inputs() -> void; + }; +} // namespace nes \ No newline at end of file diff --git a/lib/nes/common/CMakeLists.txt b/lib/nes/common/CMakeLists.txt index 69ae435..553d468 100644 --- a/lib/nes/common/CMakeLists.txt +++ b/lib/nes/common/CMakeLists.txt @@ -3,4 +3,5 @@ target_sources( PRIVATE debug.hh rgb.hh + list.hh display.hh) \ No newline at end of file diff --git a/lib/nes/common/list.hh b/lib/nes/common/list.hh new file mode 100644 index 0000000..5f0d258 --- /dev/null +++ b/lib/nes/common/list.hh @@ -0,0 +1,130 @@ +#pragma once + +namespace nes +{ + /// An intrusive doubly-linked list. + template + class list + { + public: + /// Base class of nodes in the list. + class node + { + friend list; + + list* list_{ nullptr }; + T* prev_{ nullptr }; + T* next_{ nullptr }; + + public: + virtual ~node() = default; + + node(node const&) = delete; + node(node&&) = delete; + auto operator=(node const&) -> node& = delete; + auto operator=(node&&) -> node& = delete; + + protected: + explicit node() = default; + }; + + /// An iterator for the linked list. + class iterator + { + friend list; + + T* current_{ nullptr }; + + public: + auto operator==(iterator const& other) -> bool { return current_ == other.current_; } + auto operator!=(iterator const& other) -> bool { return current_ != other.current_; } + + auto operator*() -> T* { return current_; } + + auto operator++() -> iterator& + { + if (current_) + { + node& current_node = *current_; + current_ = current_node.next_; + } + return *this; + } + + auto operator++(int) -> iterator + { + auto res = *this; + ++*this; + return res; + } + + private: + explicit iterator() = default; + explicit iterator(T* current) + : current_{ current } + { + } + }; + + private: + T* head_{ nullptr }; + T* tail_{ nullptr }; + + public: + explicit list() = default; + + ~list() + { + while (begin() != end()) { remove(begin()); } + } + + list(list const&) = delete; + list(list&&) = delete; + auto operator=(list const&) -> list& = delete; + auto operator=(list&&) -> list& = delete; + + auto begin() const -> iterator { return iterator{ head_ }; } + auto end() const -> iterator { return iterator{}; } + + /// Add an item to the end of the list, removing it from any other list it may be in. + auto append(T& item) -> void + { + node& item_node = item; + if (item_node.list_) { item_node.list_->remove(item); } + + item_node.list_ = this; + if (tail_) + { + item_node.prev_ = tail_; + tail_->next_ = &item; + } + else + { + head_ = &item; + } + tail_ = &item; + } + + /// Remove an item from the list. + auto remove(T& item) -> void + { + node& item_node = item; + if (!item_node.list_) { return; } + + if (head_ == &item) { head_ = item_node.next_; } + if (tail_ == &item) { tail_ = item_node.prev_; } + if (item_node.prev_) { item_node.prev_->next_ = item_node.next_; } + if (item_node.next_) { item_node.next_->prev_ = item_node.prev_; } + + item_node.next_ = nullptr; + item_node.prev_ = nullptr; + item_node.list_ = nullptr; + } + + /// Remove an item from the list by its iterator. + auto remove(iterator it) -> void + { + if (it != end()) { remove(**it); } + } + }; +} // namespace nes \ No newline at end of file -- cgit v1.3.1