diff options
| author | Hannes Schulze <haschu0103@outlook.de> | 2025-02-06 16:28:39 +0100 |
|---|---|---|
| committer | Hannes Schulze <haschu0103@outlook.de> | 2025-02-06 16:28:39 +0100 |
| commit | 30b4364c772114303d9045677186a53855ea5343 (patch) | |
| tree | 3ac3443cdea7274e688bc633fa12bf1e089d6624 | |
| parent | 64308c9e520dbd8e72b675d0ba064ecdaf9bd494 (diff) | |
implement in-game popups
25 files changed, 294 insertions, 33 deletions
diff --git a/app/mac/impl/display-spritekit.hh b/app/mac/impl/display-spritekit.hh index 64e3b03..334d492 100644 --- a/app/mac/impl/display-spritekit.hh +++ b/app/mac/impl/display-spritekit.hh @@ -13,6 +13,7 @@ namespace nes::app::mac NSMutableData* buffer_front_; NSMutableData* buffer_back_; u8* bytes_back_; + u8* bytes_front_; public: explicit display_spritekit(SKSpriteNode* node); diff --git a/app/mac/impl/display-spritekit.mm b/app/mac/impl/display-spritekit.mm index b6123d6..a5809c3 100644 --- a/app/mac/impl/display-spritekit.mm +++ b/app/mac/impl/display-spritekit.mm @@ -16,13 +16,14 @@ namespace nes::app::mac , buffer_front_{ make_buffer() } , buffer_back_{ make_buffer() } , bytes_back_{ static_cast<u8*>([buffer_back_ mutableBytes]) } + , bytes_front_{ static_cast<u8*>([buffer_front_ mutableBytes]) } { } auto display_spritekit::switch_buffers() -> void { std::swap(buffer_back_, buffer_front_); - bytes_back_ = static_cast<u8*>([buffer_back_ mutableBytes]); + std::swap(bytes_back_, bytes_front_); auto texture = [SKTexture textureWithData:buffer_front_ size:CGSizeMake(width, height) flipped:YES]; [texture setFilteringMode:SKTextureFilteringNearest]; @@ -42,9 +43,9 @@ namespace nes::app::mac { auto const offset = (y * width + x) * 4; auto res = rgb{}; - res.r = bytes_back_[offset + 0]; - res.g = bytes_back_[offset + 1]; - res.b = bytes_back_[offset + 2]; + res.r = bytes_front_[offset + 0]; + res.g = bytes_front_[offset + 1]; + res.b = bytes_front_[offset + 2]; return res; } } // namespace nes::app::mac
\ No newline at end of file diff --git a/lib/nes/app/action.hh b/lib/nes/app/action.hh index f63c17d..cca7a35 100644 --- a/lib/nes/app/action.hh +++ b/lib/nes/app/action.hh @@ -17,6 +17,8 @@ namespace nes::app go_to_settings, go_to_help, close_popup, + cancel_quit, + confirm_quit, launch_game, show_error, }; @@ -35,6 +37,8 @@ namespace nes::app static auto go_to_settings() -> action { return action{ type::go_to_settings }; } static auto go_to_help() -> action { return action{ type::go_to_help }; } static auto close_popup() -> action { return action{ type::close_popup }; } + static auto cancel_quit() -> action { return action{ type::cancel_quit }; } + static auto confirm_quit() -> action { return action{ type::confirm_quit }; } static auto launch_game(std::string_view const file_name) -> action { diff --git a/lib/nes/app/application.cc b/lib/nes/app/application.cc index ac81840..01d3f0b 100644 --- a/lib/nes/app/application.cc +++ b/lib/nes/app/application.cc @@ -4,7 +4,6 @@ #include "nes/app/ui/screen.hh" #include "nes/app/graphics/renderer.hh" #include "nes/app/action.hh" -#include <iostream> namespace nes::app { @@ -32,6 +31,7 @@ namespace nes::app , screen_title_{ keyboard } , screen_browser_{ keyboard, file_browser } , screen_error_{ keyboard } + , screen_confirm_quit_{ keyboard } { display_.screen = &screen_title_; } @@ -49,8 +49,29 @@ namespace nes::app handle_action(a); } - if (console_) + if (display_.screen) { + // Directly let the scene render itself without rendering any gameplay (the scene is responsible for + // clearing the buffer). + display_.switch_buffers(); + } + else if (console_) + { + // Handle escape key. + while (auto const event = input_manager_.get_keyboard().poll_event()) + { + if (event == input_event::key_down(key::escape)) + { + screen_freeze_.freeze(display_); + screen_confirm_quit_.set_confirm(false); + display_.screen = &screen_freeze_; + display_.popup = &screen_confirm_quit_; + display_.switch_buffers(); + return; + } + } + + // Forward current input state to the NES. console_->ref_controller_1().set_pressed(input_manager_.get_input_1().read_buttons()); console_->ref_controller_2().set_pressed(input_manager_.get_input_2().read_buttons()); @@ -60,22 +81,15 @@ namespace nes::app if (console_->get_status() != status::success) { - // TODO: Present as error popup - std::cerr << "Invalid state: " << to_string(console_->get_status()) << std::endl; - std::abort(); + screen_freeze_.freeze(display_); + display_.screen = &screen_freeze_; + show_error("Runtime error", console_->get_status(), action::go_to_browser()); + console_.clear(); } - - // TODO: Handle escape key - } - else - { - // Directly let the scene render itself without rendering any gameplay (the scene is responsible for - // clearing the buffer). - display_.switch_buffers(); } } - auto application::handle_action(action const a) -> void + auto application::handle_action(action const& a) -> void { switch (a.get_type()) { @@ -112,6 +126,19 @@ namespace nes::app display_.popup = nullptr; break; } + case action::type::cancel_quit: + { + display_.popup = nullptr; + display_.screen = nullptr; + break; + } + case action::type::confirm_quit: + { + display_.popup = nullptr; + display_.screen = &screen_browser_; + console_.clear(); + break; + } case action::type::launch_game: { u8 buffer[sys::cartridge::max_file_size]; @@ -142,11 +169,11 @@ namespace nes::app } } - auto application::show_error(std::string_view const message, status const error) -> void + auto application::show_error(std::string_view const message, status const error, action const& action) -> void { screen_error_.set_message(message); screen_error_.set_error(error); + screen_error_.set_action(action); display_.popup = &screen_error_; } - } // namespace nes::app
\ No newline at end of file diff --git a/lib/nes/app/application.hh b/lib/nes/app/application.hh index 81e6516..ddeff85 100644 --- a/lib/nes/app/application.hh +++ b/lib/nes/app/application.hh @@ -3,7 +3,9 @@ #include "nes/app/input/input-manager.hh" #include "nes/app/ui/screen-title.hh" #include "nes/app/ui/screen-browser.hh" +#include "nes/app/ui/screen-freeze.hh" #include "nes/app/ui/screen-error.hh" +#include "nes/app/ui/screen-confirm-quit.hh" #include "nes/sys/nes.hh" #include "nes/common/box.hh" #include "nes/common/display.hh" @@ -47,7 +49,9 @@ namespace nes::app box<sys::nes> console_{}; screen_title screen_title_; screen_browser screen_browser_; + screen_freeze screen_freeze_; screen_error screen_error_; + screen_confirm_quit screen_confirm_quit_; public: explicit application(display&, input_device_keyboard&, file_browser& file_browser); @@ -64,7 +68,7 @@ namespace nes::app auto remove_controller(input_device_controller& c) -> void { input_manager_.remove_controller(c); } private: - auto handle_action(action) -> void; - auto show_error(std::string_view message, status error) -> void; + auto handle_action(action const&) -> void; + auto show_error(std::string_view message, status error, action const& action = action::close_popup()) -> void; }; } // namespace nes::app
\ No newline at end of file diff --git a/lib/nes/app/graphics/renderer.cc b/lib/nes/app/graphics/renderer.cc index d512e71..564125b 100644 --- a/lib/nes/app/graphics/renderer.cc +++ b/lib/nes/app/graphics/renderer.cc @@ -5,6 +5,7 @@ #include "nes/app/graphics/image-view.hh" #include "nes/app/graphics/text-attributes.hh" #include "nes/app/graphics/tiles/characters.hh" +#include "nes/app/graphics/tiles/borders.hh" #include "nes/common/display.hh" namespace nes::app @@ -125,11 +126,9 @@ namespace nes::app } } - auto renderer::render_rect( - i32 const x, i32 const y, u32 const width, u32 const height, color const c) -> void + auto renderer::render_rect(i32 const x, i32 const y, u32 const width, u32 const height, color const c) -> void { if (c == color::transparent) { return; } - if (x >= static_cast<i32>(renderer::width) || y >= static_cast<i32>(renderer::height)) { return; } auto const resolved = resolve_color(c); auto const x_stop = std::min(x + static_cast<i32>(width), static_cast<i32>(renderer::width)); @@ -145,6 +144,55 @@ namespace nes::app } } + auto renderer::render_border(i32 const x, i32 const y, u32 const width, u32 const height, color const c) -> void + { + if (width <= 1 || height <= 1) { return; } + + auto const x_last = std::min(x + static_cast<i32>(width), static_cast<i32>(renderer::width)) - 1; + auto const y_last = std::min(y + static_cast<i32>(height), static_cast<i32>(renderer::height)) - 1; + auto const x_start = std::max(x, 0); + auto const y_start = std::max(y, 0); + for (auto border_y = i32{ y_start }; border_y <= y_last; ++border_y) + { + for (auto border_x = i32{ x_start }; border_x <= x_last; ++border_x) + { + if (border_x == x_start && border_y == y_start) + { + render_mask_tile(border_x, border_y, tiles::border_top_left, c); + } + else if (border_x == x_last && border_y == y_start) + { + render_mask_tile(border_x, border_y, tiles::border_top_right, c); + } + else if (border_x == x_last && border_y == y_last) + { + render_mask_tile(border_x, border_y, tiles::border_bottom_right, c); + } + else if (border_x == x_start && border_y == y_last) + { + render_mask_tile(border_x, border_y, tiles::border_bottom_left, c); + } + else if (border_x == x_start) + { + render_mask_tile(border_x, border_y, tiles::border_left, c); + } + else if (border_x == x_last) + { + render_mask_tile(border_x, border_y, tiles::border_right, c); + } + else if (border_y == y_start) + { + render_mask_tile(border_x, border_y, tiles::border_top, c); + } + else if (border_y == y_last) + { + render_mask_tile(border_x, border_y, tiles::border_bottom, c); + } + } + } + } + + auto renderer::render_fill(color const c) -> void { render_rect(0, 0, width, height, c); diff --git a/lib/nes/app/graphics/renderer.hh b/lib/nes/app/graphics/renderer.hh index 9b764a6..83cafb3 100644 --- a/lib/nes/app/graphics/renderer.hh +++ b/lib/nes/app/graphics/renderer.hh @@ -37,6 +37,8 @@ namespace nes::app auto operator=(renderer const&) -> renderer& = delete; auto operator=(renderer&&) -> renderer& = delete; + auto get_display() const -> display& { return display_; } + /// Render a colored tile at the given tile coordinates. auto render_image_tile(i32 x, i32 y, image_tile const&) -> void; /// Render an alpha-only tile at the given tile coordinates with the given color. @@ -66,6 +68,8 @@ namespace nes::app /// Render a rectangle at the given tile coordinates with a given size (measured in tiles). auto render_rect(i32 x, i32 y, u32 width, u32 height, color) -> void; + /// Render a border around a rectangle with the given size (measured in tiles). + auto render_border(i32 x, i32 y, u32 width, u32 height, color) -> void; /// Completely fill the screen. auto render_fill(color) -> void; diff --git a/lib/nes/app/graphics/tiles/CMakeLists.txt b/lib/nes/app/graphics/tiles/CMakeLists.txt index 144b9da..eced5ca 100644 --- a/lib/nes/app/graphics/tiles/CMakeLists.txt +++ b/lib/nes/app/graphics/tiles/CMakeLists.txt @@ -74,6 +74,20 @@ nes_generate_tiles( nes_generate_tiles( nes + borders + TYPE mask + FILES + border_top.png + border_top_left.png + border_top_right.png + border_left.png + border_right.png + border_bottom.png + border_bottom_left.png + border_bottom_right.png) + +nes_generate_tiles( + nes banner TYPE image FILES banner.png)
\ No newline at end of file diff --git a/lib/nes/app/graphics/tiles/border_bottom.png b/lib/nes/app/graphics/tiles/border_bottom.png Binary files differnew file mode 100644 index 0000000..05ac7a7 --- /dev/null +++ b/lib/nes/app/graphics/tiles/border_bottom.png diff --git a/lib/nes/app/graphics/tiles/border_bottom_left.png b/lib/nes/app/graphics/tiles/border_bottom_left.png Binary files differnew file mode 100644 index 0000000..974b1b1 --- /dev/null +++ b/lib/nes/app/graphics/tiles/border_bottom_left.png diff --git a/lib/nes/app/graphics/tiles/border_bottom_right.png b/lib/nes/app/graphics/tiles/border_bottom_right.png Binary files differnew file mode 100644 index 0000000..2fa6822 --- /dev/null +++ b/lib/nes/app/graphics/tiles/border_bottom_right.png diff --git a/lib/nes/app/graphics/tiles/border_left.png b/lib/nes/app/graphics/tiles/border_left.png Binary files differnew file mode 100644 index 0000000..ba5ecb6 --- /dev/null +++ b/lib/nes/app/graphics/tiles/border_left.png diff --git a/lib/nes/app/graphics/tiles/border_right.png b/lib/nes/app/graphics/tiles/border_right.png Binary files differnew file mode 100644 index 0000000..801bd7d --- /dev/null +++ b/lib/nes/app/graphics/tiles/border_right.png diff --git a/lib/nes/app/graphics/tiles/border_top.png b/lib/nes/app/graphics/tiles/border_top.png Binary files differnew file mode 100644 index 0000000..94d5cf5 --- /dev/null +++ b/lib/nes/app/graphics/tiles/border_top.png diff --git a/lib/nes/app/graphics/tiles/border_top_left.png b/lib/nes/app/graphics/tiles/border_top_left.png Binary files differnew file mode 100644 index 0000000..f67b585 --- /dev/null +++ b/lib/nes/app/graphics/tiles/border_top_left.png diff --git a/lib/nes/app/graphics/tiles/border_top_right.png b/lib/nes/app/graphics/tiles/border_top_right.png Binary files differnew file mode 100644 index 0000000..0b8613c --- /dev/null +++ b/lib/nes/app/graphics/tiles/border_top_right.png diff --git a/lib/nes/app/ui/CMakeLists.txt b/lib/nes/app/ui/CMakeLists.txt index de42744..37720c1 100644 --- a/lib/nes/app/ui/CMakeLists.txt +++ b/lib/nes/app/ui/CMakeLists.txt @@ -7,5 +7,9 @@ target_sources( screen-title.cc screen-browser.hh screen-browser.cc + screen-freeze.hh + screen-freeze.cc screen-error.hh - screen-error.cc)
\ No newline at end of file + screen-error.cc + screen-confirm-quit.hh + screen-confirm-quit.cc)
\ No newline at end of file diff --git a/lib/nes/app/ui/screen-confirm-quit.cc b/lib/nes/app/ui/screen-confirm-quit.cc new file mode 100644 index 0000000..3c7a337 --- /dev/null +++ b/lib/nes/app/ui/screen-confirm-quit.cc @@ -0,0 +1,68 @@ +#include "nes/app/ui/screen-confirm-quit.hh" +#include "nes/app/graphics/renderer.hh" +#include "nes/app/graphics/color.hh" +#include "nes/app/input/input-device-keyboard.hh" + +namespace nes::app +{ + screen_confirm_quit::screen_confirm_quit(input_device_keyboard& keyboard) + : keyboard_{ keyboard } + { + } + + auto screen_confirm_quit::render(renderer& renderer) -> void + { + renderer.render_rect(2, 8, 28, 10, color::background_secondary); + renderer.render_border(2, 8, 28, 10, color::foreground_primary); + + auto const attrs = text_attributes{} + .set_alignment(text_alignment::center) + .set_ellipsize_mode(ellipsize_mode::end) + .set_max_width(26); + renderer.render_text(16, 10, "Are you sure?", color::foreground_primary, attrs); + renderer.render_text(16, 12, "All progress is lost.", color::foreground_primary, attrs); + if (confirm_) + { + renderer.render_text(10, 15, "Cancel", color::foreground_secondary); + renderer.render_text(18, 15, "> OK <", color::accent_primary); + } + else + { + renderer.render_text(8, 15, "> Cancel <", color::accent_primary); + renderer.render_text(20, 15, "OK", color::foreground_secondary); + } + } + + auto screen_confirm_quit::process_events() -> action + { + while (auto const event = keyboard_.poll_event()) + { + // Use key-up so the enter key is not immediately captured by the game. + if (event == input_event::key_up(key::enter)) + { + if (confirm_) + { + return action::confirm_quit(); + } + else + { + return action::cancel_quit(); + } + } + else if (event == input_event::key_down(key::space)) + { + confirm_ = !confirm_; + } + else if (event == input_event::key_down(key::arrow_left)) + { + confirm_ = false; + } + else if (event == input_event::key_down(key::arrow_right)) + { + confirm_ = true; + } + } + + return action{}; + } +} // namespace nes::app
\ No newline at end of file diff --git a/lib/nes/app/ui/screen-confirm-quit.hh b/lib/nes/app/ui/screen-confirm-quit.hh new file mode 100644 index 0000000..c176f4b --- /dev/null +++ b/lib/nes/app/ui/screen-confirm-quit.hh @@ -0,0 +1,24 @@ +#pragma once + +#include "nes/app/ui/screen.hh" + +namespace nes::app +{ + class input_device_keyboard; + + /// A popup asking the user if they really want to quit the game. + class screen_confirm_quit final : public screen + { + input_device_keyboard& keyboard_; + bool confirm_{ false }; + + public: + explicit screen_confirm_quit(input_device_keyboard&); + + auto get_confirm() const -> bool { return confirm_; } + auto set_confirm(bool const v) -> void { confirm_ = v; } + + auto render(renderer&) -> void override; + auto process_events() -> action override; + }; +} // namespace nes::app
\ No newline at end of file diff --git a/lib/nes/app/ui/screen-error.cc b/lib/nes/app/ui/screen-error.cc index 67a3244..9375012 100644 --- a/lib/nes/app/ui/screen-error.cc +++ b/lib/nes/app/ui/screen-error.cc @@ -12,8 +12,8 @@ namespace nes::app auto screen_error::render(renderer& renderer) -> void { - // TODO: Nicer box - renderer.render_rect(2, 9, 28, 5, color::background_secondary); + renderer.render_rect(2, 8, 28, 10, color::background_secondary); + renderer.render_border(2, 8, 28, 10, color::foreground_primary); auto const attrs = text_attributes{} .set_alignment(text_alignment::center) @@ -21,6 +21,7 @@ namespace nes::app .set_max_width(26); renderer.render_text_format(16, 10, color::foreground_primary, attrs, "{}:", message_); renderer.render_text(16, 12, to_string(error_), color::foreground_primary, attrs); + renderer.render_text(16, 15, "> OK <", color::accent_primary, attrs); } auto screen_error::process_events() -> action @@ -29,7 +30,7 @@ namespace nes::app { if (event == input_event::key_down(key::enter)) { - return action::close_popup(); + return action_; } } diff --git a/lib/nes/app/ui/screen-error.hh b/lib/nes/app/ui/screen-error.hh index ec877e1..4559942 100644 --- a/lib/nes/app/ui/screen-error.hh +++ b/lib/nes/app/ui/screen-error.hh @@ -12,14 +12,17 @@ namespace nes::app input_device_keyboard& keyboard_; std::string_view message_; status error_{ status::success }; + action action_{}; public: explicit screen_error(input_device_keyboard&); auto get_message() const -> std::string_view { return message_; } auto get_error() const -> status { return error_; } - auto set_message(std::string_view const v) { message_ = v; } - auto set_error(status const v) { error_ = v; } + auto get_action() const -> action { return action_; } + auto set_message(std::string_view const v) -> void { message_ = v; } + auto set_error(status const v) -> void { error_ = v; } + auto set_action(action const& v) -> void { action_ = v; } auto render(renderer&) -> void override; auto process_events() -> action override; diff --git a/lib/nes/app/ui/screen-freeze.cc b/lib/nes/app/ui/screen-freeze.cc new file mode 100644 index 0000000..60410dd --- /dev/null +++ b/lib/nes/app/ui/screen-freeze.cc @@ -0,0 +1,33 @@ +#include "nes/app/ui/screen-freeze.hh" +#include "nes/app/graphics/renderer.hh" + +namespace nes::app +{ + auto screen_freeze::freeze(display const& disp) -> void + { + for (auto y = u32{ 0 }; y < display::height; ++y) + { + for (auto x = u32{ 0 }; x < display::width; ++x) + { + frozen_data_[y * display::width + x] = disp.get(x, y); + } + } + } + + auto screen_freeze::render(renderer& renderer) -> void + { + auto& disp = renderer.get_display(); + for (auto y = u32{ 0 }; y < display::height; ++y) + { + for (auto x = u32{ 0 }; x < display::width; ++x) + { + disp.set(x, y, frozen_data_[y * display::width + x]); + } + } + } + + auto screen_freeze::process_events() -> action + { + return action{}; + } +} // namespace nes::app
\ No newline at end of file diff --git a/lib/nes/app/ui/screen-freeze.hh b/lib/nes/app/ui/screen-freeze.hh new file mode 100644 index 0000000..fe67aad --- /dev/null +++ b/lib/nes/app/ui/screen-freeze.hh @@ -0,0 +1,25 @@ +#pragma once + +#include "nes/app/ui/screen.hh" +#include "nes/common/rgb.hh" +#include "nes/common/display.hh" + +namespace nes::app +{ + /// Freeze screen which preserves the display buffer at a certain state. + /// + /// This is usually used to preserve the image from the console when the game should not continue. + class screen_freeze final : public screen + { + rgb frozen_data_[display::width * display::height]{}; + + public: + explicit screen_freeze() = default; + + auto render(renderer&) -> void override; + auto process_events() -> action override; + + /// Update the image shown to the back buffer of the given display. + auto freeze(display const&) -> void; + }; +} // namespace nes::app
\ No newline at end of file diff --git a/lib/nes/common/display.hh b/lib/nes/common/display.hh index eb35366..f7d53c1 100644 --- a/lib/nes/common/display.hh +++ b/lib/nes/common/display.hh @@ -25,7 +25,7 @@ namespace nes virtual auto switch_buffers() -> void = 0; /// Update the pixel at the given position in the back buffer. virtual auto set(u32 x, u32 y, rgb value) -> void = 0; - /// Load the pixel value at the given position in the back buffer. + /// Load the pixel value at the given position in the front buffer. virtual auto get(u32 x, u32 y) const -> rgb = 0; protected: diff --git a/lib/nes/common/rgb.hh b/lib/nes/common/rgb.hh index 0b8ec1c..b8cd0f3 100644 --- a/lib/nes/common/rgb.hh +++ b/lib/nes/common/rgb.hh @@ -8,7 +8,7 @@ namespace nes class rgb { public: - explicit rgb() = default; + rgb() = default; explicit rgb(u8 const r, u8 const g, u8 const b) : r{ r } |
