diff --git a/CMakeLists.txt b/CMakeLists.txt index d2baa48..636e427 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,6 +7,8 @@ add_library(boundard STATIC src/game.cpp src/scene.cpp src/render.cpp + src/render_loop.cpp + src/input.cpp src/config.cpp src/engine.cpp ) diff --git a/examples/square/config.ceg b/examples/square/config.ceg index 2f99edf..2529ece 100644 --- a/examples/square/config.ceg +++ b/examples/square/config.ceg @@ -8,10 +8,10 @@ "input": [ { "action": "quit", "keys": ["Escape"] }, { "action": "switch_scene", "keys": ["Tab"] }, - { "action": "move_left", "keys": ["Left"] }, - { "action": "move_right", "keys": ["Right"] }, - { "action": "move_up", "keys": ["Up"] }, - { "action": "move_down", "keys": ["Down"] } + { "action": "move_left", "keys": ["a"] }, + { "action": "move_right", "keys": ["d"] }, + { "action": "move_up", "keys": ["w"] }, + { "action": "move_down", "keys": ["s"] } ], "start_scene": "square", diff --git a/examples/square/main.cpp b/examples/square/main.cpp index ee0ae0d..ba85d05 100644 --- a/examples/square/main.cpp +++ b/examples/square/main.cpp @@ -19,10 +19,6 @@ void register_square_behaviors() { "square_tick", [](boundard::Actor& actor, float dt, const boundard::render::InputMap& input) { const float t = actor.age(); - actor.set_facing({ - std::cos(t * 1.5F), - std::sin(t * 1.5F), - }); if (actor.mesh() != nullptr) { actor.mesh()->set_color({ @@ -34,7 +30,7 @@ void register_square_behaviors() { } auto position = actor.position(); - const float move_speed = 0.7F; + constexpr float move_speed = 0.7F; position[0] += input.get_axis("move_left", "move_right") * move_speed * dt; position[1] += input.get_axis("move_up", "move_down") * move_speed * dt; actor.set_position(position); @@ -54,12 +50,8 @@ void register_square_behaviors() { behaviors.register_tick( "square_alt_tick", - [](boundard::Actor& actor, float dt, const boundard::render::InputMap& input) { + [](boundard::Actor& actor, const float dt, const boundard::render::InputMap& input) { const float t = actor.age(); - actor.set_facing({ - std::sin(t * 1.3F), - std::cos(t * 1.3F), - }); if (actor.mesh() != nullptr) { actor.mesh()->set_color({ @@ -71,7 +63,7 @@ void register_square_behaviors() { } auto position = actor.position(); - const float move_speed = 0.7F; + constexpr float move_speed = 0.7F; position[0] += input.get_axis("move_left", "move_right") * move_speed * dt; position[1] += input.get_axis("move_up", "move_down") * move_speed * dt; actor.set_position(position); diff --git a/examples/square/scenes/square.json b/examples/square/scenes/square.json index 1d70b83..b142f6a 100644 --- a/examples/square/scenes/square.json +++ b/examples/square/scenes/square.json @@ -1,21 +1,31 @@ { - "actors": [ - { - "name": "square", - "init": "square_init", - "tick": "square_tick", - "position": { - "x": 0, - "y": 0 - }, - "facing": { - "x": 1, - "y": 0 - }, - "mesh": { - "shape": "quad", - "texture": "Dank.png" - } - } - ] + "default_camera": "square_camera", + "grid_background": true, + "actors": [ + { + "name": "square", + "init": "square_init", + "tick": "square_tick", + "position": { + "x": 0, + "y": 0 + }, + "facing": { + "x": 1, + "y": 0 + }, + "mesh": { + "shape": "quad", + "texture": "Dank.png" + }, + "children": [ + { + "name": "square_camera", + "type": "camera", + "position": { "x": 0, "y": 0 }, + "zoom": 0.2 + } + ] + } + ] } diff --git a/examples/square/scenes/square_alt.json b/examples/square/scenes/square_alt.json index e3c5189..29027a1 100644 --- a/examples/square/scenes/square_alt.json +++ b/examples/square/scenes/square_alt.json @@ -1,4 +1,6 @@ { + "default_camera": "square_alt_camera", + "grid_background": true, "actors": [ { "name": "square_alt", @@ -9,7 +11,15 @@ "mesh": { "shape": "quad", "color": { "r": 0.9, "g": 0.3, "b": 0.2, "a": 1.0 } - } + }, + "children": [ + { + "name": "square_alt_camera", + "type": "camera", + "position": { "x": 0, "y": 0 }, + "zoom": 1.0 + } + ] } ] } diff --git a/include/actor.hpp b/include/actor.hpp index 5b852a4..4db1112 100644 --- a/include/actor.hpp +++ b/include/actor.hpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include @@ -15,14 +16,16 @@ class InputMap; namespace boundard { -class Actor { +class Actor : public std::enable_shared_from_this { private: size_t id; std::string nickName_; mutable std::mutex transform_mutex_; std::array position_{0.0F, 0.0F}; std::array facing_{1.0F, 0.0F}; - std::shared_ptr mesh_; + mutable std::mutex tree_mutex_; + std::weak_ptr parent_; + std::vector> children_; float age_ = 0.0F; bool initialized_ = false; @@ -43,11 +46,6 @@ class Actor { nickName_ = std::move(nickName); } - Actor(std::string nickName, Game& game, std::shared_ptr mesh) - : Actor(std::move(nickName), game) { - mesh_ = std::move(mesh); - } - static Actor& getInstance() { static Actor instance; return instance; @@ -75,6 +73,9 @@ class Actor { if (init_handler_) { init_handler_(*this); } + for (const std::shared_ptr& child : children()) { + child->initialize(); + } } // 类似 Godot 的 _process(delta)。 @@ -83,6 +84,9 @@ class Actor { if (tick_handler_) { tick_handler_(*this, dt, input); } + for (const std::shared_ptr& child : children()) { + child->process(dt, input); + } } std::array position() const { @@ -95,6 +99,19 @@ class Actor { position_ = position; } + std::array global_position() const { + const std::array local_position = position(); + const std::shared_ptr parent = this->parent(); + if (parent == nullptr) return local_position; + + const std::array parent_position = parent->global_position(); + const std::array parent_facing = parent->global_facing(); + return {parent_position[0] + parent_facing[0] * local_position[0] - + parent_facing[1] * local_position[1], + parent_position[1] + parent_facing[1] * local_position[0] + + parent_facing[0] * local_position[1]}; + } + std::array facing() const { std::lock_guard lock(transform_mutex_); return facing_; @@ -105,8 +122,67 @@ class Actor { facing_ = facing; } - const std::shared_ptr& mesh() const { return mesh_; } - void set_mesh(std::shared_ptr mesh) { mesh_ = std::move(mesh); } + std::array global_facing() const { + std::array local_facing = facing(); + const std::shared_ptr parent = this->parent(); + if (parent == nullptr) return local_facing; + + const std::array parent_facing = parent->global_facing(); + return {parent_facing[0] * local_facing[0] - parent_facing[1] * local_facing[1], + parent_facing[1] * local_facing[0] + parent_facing[0] * local_facing[1]}; + } + + virtual const std::shared_ptr& mesh() const { + static const std::shared_ptr no_mesh; + return no_mesh; + } + virtual void set_mesh(std::shared_ptr) {} + + std::shared_ptr parent() const { + std::lock_guard lock(tree_mutex_); + return parent_.lock(); + } + + std::vector> children() const { + std::lock_guard lock(tree_mutex_); + return children_; + } + + bool add_child(const std::shared_ptr& child) { + if (child == nullptr) return false; + const std::shared_ptr self = shared_from_this(); + if (child == self) return false; + for (std::shared_ptr ancestor = self; ancestor != nullptr; + ancestor = ancestor->parent()) { + if (ancestor == child) return false; + } + + if (const std::shared_ptr old_parent = child->parent()) { + old_parent->remove_child(child); + } + { + std::lock_guard lock(tree_mutex_); + children_.push_back(child); + } + { + std::lock_guard lock(child->tree_mutex_); + child->parent_ = self; + } + return true; + } + + bool remove_child(const std::shared_ptr& child) { + if (child == nullptr) return false; + std::lock_guard lock(tree_mutex_); + for (auto it = children_.begin(); it != children_.end(); ++it) { + if (*it != child) continue; + children_.erase(it); + std::lock_guard child_lock(child->tree_mutex_); + child->parent_.reset(); + return true; + } + return false; + } }; } // namespace boundard diff --git a/include/boundard.hpp b/include/boundard.hpp index db879cf..b9df25d 100644 --- a/include/boundard.hpp +++ b/include/boundard.hpp @@ -3,6 +3,8 @@ #include #include #include +#include +#include #include #include #include diff --git a/include/camera.hpp b/include/camera.hpp new file mode 100644 index 0000000..2a2165c --- /dev/null +++ b/include/camera.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include + +#include + +namespace boundard { + +class Camera : public Actor { + public: + Camera(std::string name, Game& game) : Actor(std::move(name), game) {} + + float zoom() const { return zoom_; } + void set_zoom(float zoom) { zoom_ = zoom > 0.0F ? zoom : 0.0001F; } + + private: + float zoom_ = 1.0F; +}; + +} // namespace boundard diff --git a/include/config.hpp b/include/config.hpp index eb6e75a..3427cc8 100644 --- a/include/config.hpp +++ b/include/config.hpp @@ -21,11 +21,14 @@ struct MeshConfig { struct ActorConfig { std::string name; + std::string type = "actor"; std::string init_function; std::string tick_function; std::array position{0.0F, 0.0F}; std::array facing{1.0F, 0.0F}; + float zoom = 1.0F; MeshConfig mesh; + std::vector children; }; struct InputBindingConfig { @@ -38,6 +41,8 @@ struct SceneConfig { std::string name; std::string path; std::vector actors; + std::string default_camera; + bool grid_background = false; }; struct GameConfig { diff --git a/include/mesh.hpp b/include/mesh.hpp index 341c632..77fbcc8 100644 --- a/include/mesh.hpp +++ b/include/mesh.hpp @@ -44,6 +44,33 @@ class Mesh { return mesh; } + static Mesh grid(int half_cells = 12) { + Mesh mesh; + const float world_to_local = 1.0F / 0.35F; + const float half_extent = static_cast(half_cells) * world_to_local; + const float line_half_width = 0.012F * world_to_local; + const std::array line_color{1.0F, 1.0F, 1.0F, 1.0F}; + auto add_rect = [&](float left, float bottom, float right, float top) { + const uint32_t base = static_cast(mesh.vertices.size()); + mesh.vertices.insert(mesh.vertices.end(), { + {{left, bottom}, line_color, {0.0F, 0.0F}}, + {{right, bottom}, line_color, {1.0F, 0.0F}}, + {{right, top}, line_color, {1.0F, 1.0F}}, + {{left, top}, line_color, {0.0F, 1.0F}}, + }); + mesh.indices.insert(mesh.indices.end(), + {base, base + 1, base + 2, base + 2, base + 3, base}); + }; + for (int cell = -half_cells; cell <= half_cells; ++cell) { + const float coordinate = static_cast(cell) * world_to_local; + add_rect(coordinate - line_half_width, -half_extent, coordinate + line_half_width, + half_extent); + add_rect(-half_extent, coordinate - line_half_width, half_extent, + coordinate + line_half_width); + } + return mesh; + } + private: std::unique_ptr color_mutex_ = std::make_unique(); std::array color_{1.0F, 1.0F, 1.0F, 1.0F}; diff --git a/include/render.hpp b/include/render.hpp index 5510f09..6e3d35b 100644 --- a/include/render.hpp +++ b/include/render.hpp @@ -13,6 +13,10 @@ #include +namespace boundard { +class Camera; +} + namespace boundard::render { struct InputEvent { @@ -51,6 +55,7 @@ class Renderer { void init(int width, int height, const char* title); void draw(const Actor* const* actors, size_t actor_count); + void draw(const Actor* const* actors, size_t actor_count, const Camera* camera); bool should_close() const; void poll_events() const; @@ -119,6 +124,7 @@ class RenderLoop { void stop(); void set_actors(std::vector actors); + void set_camera(Camera* camera); void poll_events() const; bool should_close() const; @@ -134,6 +140,7 @@ class RenderLoop { std::atomic running_{false}; mutable std::mutex actors_mutex_; std::vector actors_; + Camera* camera_ = nullptr; }; } // namespace boundard::render diff --git a/include/scene.hpp b/include/scene.hpp index 79bcd94..364a3c1 100644 --- a/include/scene.hpp +++ b/include/scene.hpp @@ -11,6 +11,7 @@ class InputMap; namespace boundard { class Actor; +class Camera; // 运行时场景:持有场景名和其中的 Actor,并统一驱动 Actor 的 init/tick。 class Scene { @@ -30,6 +31,9 @@ class Scene { void set_actors(std::vector> actors); void clear(); + void set_default_camera(std::string name); + Camera* primary_camera() const; + const std::vector& actors() const; std::vector& actors(); @@ -39,7 +43,8 @@ class Scene { private: std::string name_; std::vector> actor_storage_; - std::vector actors_; + mutable std::vector actors_; + std::string default_camera_; }; } // namespace boundard diff --git a/include/sprite_actor.hpp b/include/sprite_actor.hpp new file mode 100644 index 0000000..95a525f --- /dev/null +++ b/include/sprite_actor.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include + +#include + +namespace boundard { + +class SpriteActor : public Actor { + public: + SpriteActor(std::string name, Game& game, std::shared_ptr mesh) + : Actor(std::move(name), game), mesh_(std::move(mesh)) {} + + const std::shared_ptr& mesh() const override { return mesh_; } + void set_mesh(std::shared_ptr mesh) override { mesh_ = std::move(mesh); } + + private: + std::shared_ptr mesh_; +}; + +} // namespace boundard diff --git a/scenes/main.json b/scenes/main.json index 0188937..e57a85e 100644 --- a/scenes/main.json +++ b/scenes/main.json @@ -1,4 +1,6 @@ { + "default_camera": "main_camera", + "grid_background": true, "actors": [ { "name": "square", @@ -13,7 +15,16 @@ "mesh": { "shape": "quad", "texture": "Dank.png" - } + }, + "children": [ + { + "name": "main_camera", + "type": "camera", + "position": { "x": 0, "y": 0 }, + "facing": { "x": 1, "y": 0 }, + "zoom": 1.0 + } + ] } ] } diff --git a/src/config.cpp b/src/config.cpp index 6dc5975..f61aeee 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -243,6 +243,9 @@ ActorConfig parse_actor(const Json::Value& value) { if (value.isMember("name")) { actor.name = json_string(value["name"], ""); } + if (value.isMember("type")) { + actor.type = json_string(value["type"], actor.type); + } if (value.isMember("init")) { actor.init_function = json_string(value["init"], ""); } @@ -261,6 +264,9 @@ ActorConfig parse_actor(const Json::Value& value) { actor.facing[0] = json_float(facing["x"], actor.facing[0]); actor.facing[1] = json_float(facing["y"], actor.facing[1]); } + if (value.isMember("zoom")) { + actor.zoom = json_float(value["zoom"], actor.zoom); + } if (value.isMember("mesh") && value["mesh"].isObject()) { const Json::Value& mesh = value["mesh"]; @@ -272,6 +278,11 @@ ActorConfig parse_actor(const Json::Value& value) { } parse_color(mesh, actor.mesh); } + if (value.isMember("children") && value["children"].isArray()) { + for (const Json::Value& child : value["children"]) { + if (child.isObject()) actor.children.push_back(parse_actor(child)); + } + } return actor; } @@ -312,11 +323,13 @@ void extract_actors(const Json::Value& root, std::vector& actors) { void resolve_texture_paths(std::vector& actors, const std::filesystem::path& base_dir) { for (ActorConfig& actor : actors) { - if (actor.mesh.texture.empty()) continue; - std::filesystem::path texture_path(actor.mesh.texture); - if (texture_path.is_relative()) { - actor.mesh.texture = (base_dir / texture_path).lexically_normal().string(); + if (!actor.mesh.texture.empty()) { + std::filesystem::path texture_path(actor.mesh.texture); + if (texture_path.is_relative()) { + actor.mesh.texture = (base_dir / texture_path).lexically_normal().string(); + } } + resolve_texture_paths(actor.children, base_dir); } } @@ -330,6 +343,14 @@ bool parse_scene_entry(const Json::Value& item, SceneConfig& scene, if (item.isMember("name")) { scene.name = json_string(item["name"], ""); } + if (item.isMember("default_camera")) { + scene.default_camera = json_string(item["default_camera"], ""); + } else if (item.isMember("camera")) { + scene.default_camera = json_string(item["camera"], ""); + } + if (item.isMember("grid_background")) { + scene.grid_background = item["grid_background"].asBool(); + } if (scene.name.empty()) { error = "场景缺少 name"; return false; @@ -353,6 +374,14 @@ bool parse_scene_entry(const Json::Value& item, SceneConfig& scene, return false; } extract_actors(root, scene.actors); + if (root.isMember("default_camera")) { + scene.default_camera = json_string(root["default_camera"], scene.default_camera); + } else if (root.isMember("camera")) { + scene.default_camera = json_string(root["camera"], scene.default_camera); + } + if (root.isMember("grid_background")) { + scene.grid_background = root["grid_background"].asBool(); + } resolve_texture_paths(scene.actors, scene_path.parent_path()); } else { // 允许场景直接内联在游戏配置中。 diff --git a/src/engine.cpp b/src/engine.cpp index 689111c..0a64cbe 100644 --- a/src/engine.cpp +++ b/src/engine.cpp @@ -8,6 +8,8 @@ #include #include +#include +#include namespace boundard { namespace { @@ -24,9 +26,21 @@ std::shared_ptr create_actor(const ActorConfig& actor_config, Game& game, mesh->set_color(actor_config.mesh.color); mesh->texture = actor_config.mesh.texture; - auto actor = std::make_shared(actor_config.name, game, mesh); + std::shared_ptr actor; + if (actor_config.type == "camera") { + actor = std::make_shared(actor_config.name, game); + } else { + actor = std::make_shared(actor_config.name, game, mesh); + } actor->set_position(actor_config.position); actor->set_facing(actor_config.facing); + if (auto* camera = dynamic_cast(actor.get())) { + camera->set_zoom(actor_config.zoom); + } + + for (const ActorConfig& child_config : actor_config.children) { + actor->add_child(create_actor(child_config, game, behaviors)); + } if (!actor_config.init_function.empty()) { auto handler = behaviors.init(actor_config.init_function); @@ -55,10 +69,18 @@ std::shared_ptr build_scene(const SceneConfig& scene, Game& game, BehaviorRegistry& behaviors) { auto runtime_scene = std::make_shared(scene.name); + if (scene.grid_background) { + auto grid = std::make_shared("__grid", game, + std::make_shared(Mesh::grid())); + grid->mesh()->set_color({0.20F, 0.28F, 0.36F, 0.75F}); + runtime_scene->add_actor(std::move(grid)); + } + for (const ActorConfig& actor_config : scene.actors) { auto actor = create_actor(actor_config, game, behaviors); runtime_scene->add_actor(std::move(actor)); } + runtime_scene->set_default_camera(scene.default_camera); return runtime_scene; } @@ -143,6 +165,7 @@ int run_engine(int argc, char** argv, const char* default_config_path) { scene->initialize(); game.set_current_scene(scene); render_loop.set_actors(scene->actors()); + render_loop.set_camera(scene->primary_camera()); }; if (active_scene != nullptr) { diff --git a/src/input.cpp b/src/input.cpp new file mode 100644 index 0000000..0ba5ac4 --- /dev/null +++ b/src/input.cpp @@ -0,0 +1,139 @@ +#include + +namespace boundard::render { + +void InputMap::add_action(const std::string& action) { + std::lock_guard lock(mutex_); + actions_.try_emplace(action); +} + +void InputMap::bind_key(const std::string& action, int key) { + std::lock_guard lock(mutex_); + ActionState& state = actions_[action]; + state.keys.insert(key); + state.held_keys.try_emplace(key, false); +} + +void InputMap::unbind_key(const std::string& action, int key) { + std::lock_guard lock(mutex_); + auto action_it = actions_.find(action); + if (action_it == actions_.end()) return; + ActionState& state = action_it->second; + state.keys.erase(key); + auto held_it = state.held_keys.find(key); + if (held_it != state.held_keys.end() && held_it->second) { + held_it->second = false; + if (state.held_key_count > 0) --state.held_key_count; + if (state.held_key_count + state.held_button_count == 0) { + state.pressed = false; + state.just_released = true; + } + } + state.held_keys.erase(key); +} + +void InputMap::bind_mouse_button(const std::string& action, int button) { + std::lock_guard lock(mutex_); + ActionState& state = actions_[action]; + state.mouse_buttons.insert(button); + state.held_buttons.try_emplace(button, false); +} + +void InputMap::unbind_mouse_button(const std::string& action, int button) { + std::lock_guard lock(mutex_); + auto action_it = actions_.find(action); + if (action_it == actions_.end()) return; + ActionState& state = action_it->second; + state.mouse_buttons.erase(button); + auto held_it = state.held_buttons.find(button); + if (held_it != state.held_buttons.end() && held_it->second) { + held_it->second = false; + if (state.held_button_count > 0) --state.held_button_count; + if (state.held_key_count + state.held_button_count == 0) { + state.pressed = false; + state.just_released = true; + } + } + state.held_buttons.erase(button); +} + +void InputMap::handle_event(const InputEvent& event) { + std::lock_guard lock(mutex_); + for (auto& [action, state] : actions_) { + std::unordered_set* bindings = nullptr; + std::unordered_map* held_map = nullptr; + size_t* held_count = nullptr; + if (event.type == InputEvent::Type::Key) { + bindings = &state.keys; + held_map = &state.held_keys; + held_count = &state.held_key_count; + } else if (event.type == InputEvent::Type::MouseButton) { + bindings = &state.mouse_buttons; + held_map = &state.held_buttons; + held_count = &state.held_button_count; + } else { + continue; + } + const int input_id = event.type == InputEvent::Type::Key ? event.key : event.button; + if (bindings->find(input_id) == bindings->end()) continue; + if (event.action == InputEvent::Action::Press) { + bool& held = (*held_map)[input_id]; + if (!held) { + held = true; + ++(*held_count); + if (state.held_key_count + state.held_button_count == 1) { + state.pressed = true; + state.just_pressed = true; + } + } + } else if (event.action == InputEvent::Action::Release) { + auto held_it = held_map->find(input_id); + if (held_it == held_map->end() || !held_it->second) continue; + held_it->second = false; + if (*held_count > 0) --(*held_count); + if (state.held_key_count + state.held_button_count == 0) { + state.pressed = false; + state.just_released = true; + } + } + } +} + +void InputMap::end_frame() { + std::lock_guard lock(mutex_); + for (auto& [action, state] : actions_) { + state.just_pressed = false; + state.just_released = false; + } +} + +bool InputMap::is_action_pressed(const std::string& action) const { + std::lock_guard lock(mutex_); + auto it = actions_.find(action); + return it != actions_.end() && it->second.pressed; +} + +bool InputMap::is_action_just_pressed(const std::string& action) const { + std::lock_guard lock(mutex_); + auto it = actions_.find(action); + return it != actions_.end() && it->second.just_pressed; +} + +bool InputMap::is_action_just_released(const std::string& action) const { + std::lock_guard lock(mutex_); + auto it = actions_.find(action); + return it != actions_.end() && it->second.just_released; +} + +float InputMap::get_action_strength(const std::string& action) const { + std::lock_guard lock(mutex_); + auto it = actions_.find(action); + return it != actions_.end() && it->second.pressed ? 1.0F : 0.0F; +} + +float InputMap::get_axis(const std::string& negative_action, + const std::string& positive_action) const { + return get_action_strength(positive_action) - get_action_strength(negative_action); +} + +} // namespace boundard::render diff --git a/src/render.cpp b/src/render.cpp index 1633c0f..a91774b 100644 --- a/src/render.cpp +++ b/src/render.cpp @@ -1,4 +1,5 @@ #include +#include #define GLFW_INCLUDE_VULKAN #include @@ -197,6 +198,7 @@ struct Renderer::Impl { VkDescriptorPool descriptor_pool = VK_NULL_HANDLE; VkPipelineLayout pipeline_layout = VK_NULL_HANDLE; VkPipeline pipeline = VK_NULL_HANDLE; + std::atomic framebuffer_resized{false}; VkCommandPool command_pool = VK_NULL_HANDLE; std::vector frames; @@ -221,11 +223,12 @@ struct Renderer::Impl { Renderer::InputCallback input_callback; void init(int width, int height, const char* title); - void draw(const Actor* const* actors, size_t actor_count); + void draw(const Actor* const* actors, size_t actor_count, const Camera* camera); void upload_mesh(const Mesh& mesh); void create_texture(MeshResource& resource, const std::string& path); VkCommandBuffer begin_transfer(); void end_transfer(VkCommandBuffer command_buffer); + void recreate_swapchain(); void destroy(); }; @@ -240,7 +243,11 @@ void Renderer::init(int width, int height, const char* title) { } void Renderer::draw(const Actor* const* actors, size_t actor_count) { - impl_->draw(actors, actor_count); + impl_->draw(actors, actor_count, nullptr); +} + +void Renderer::draw(const Actor* const* actors, size_t actor_count, const Camera* camera) { + impl_->draw(actors, actor_count, camera); } bool Renderer::should_close() const { @@ -277,6 +284,7 @@ void Renderer::destroy() { impl_->destroy(); } +#if 0 // InputMap implementation moved to input.cpp. void InputMap::add_action(const std::string& action) { std::lock_guard lock(mutex_); actions_.try_emplace(action); @@ -428,72 +436,7 @@ float InputMap::get_axis(const std::string& negative_action, const std::string& positive_action) const { return get_action_strength(positive_action) - get_action_strength(negative_action); } - -RenderLoop::~RenderLoop() { - stop(); -} - -void RenderLoop::start(int width, int height, const char* title) { - if (running_) { - return; - } - - renderer_.init(width, height, title); - renderer_.set_input_callback([this](const InputEvent& event) { - input_map_.handle_event(event); - }); - - running_ = true; - thread_ = std::thread(&RenderLoop::run, this); -} - -void RenderLoop::stop() { - if (!running_) { - return; - } - running_ = false; - if (thread_.joinable()) { - thread_.join(); - } - renderer_.destroy(); -} - -void RenderLoop::set_actors(std::vector actors) { - std::lock_guard lock(actors_mutex_); - actors_ = std::move(actors); -} - -void RenderLoop::poll_events() const { - renderer_.poll_events(); -} - -bool RenderLoop::should_close() const { - return renderer_.should_close(); -} - -InputMap& RenderLoop::input() { - return input_map_; -} - -const InputMap& RenderLoop::input() const { - return input_map_; -} - -void RenderLoop::run() { - while (running_) { - std::vector actors; - { - std::lock_guard lock(actors_mutex_); - actors = actors_; - } - - if (!actors.empty()) { - renderer_.draw(actors.data(), actors.size()); - } else { - std::this_thread::yield(); - } - } -} +#endif void Renderer::Impl::init(int width, int height, const char* title) { if (initialized) { @@ -509,8 +452,11 @@ void Renderer::Impl::init(int width, int height, const char* title) { if (window == nullptr) { throw std::runtime_error("failed to create GLFW window"); } - glfwSetWindowUserPointer(window, this); + glfwSetFramebufferSizeCallback(window, [](GLFWwindow* target, int, int) { + auto* impl = static_cast(glfwGetWindowUserPointer(target)); + if (impl != nullptr) impl->framebuffer_resized = true; + }); glfwSetKeyCallback(window, [](GLFWwindow* target, int key, int scancode, int action, int mods) { auto* impl = static_cast(glfwGetWindowUserPointer(target)); @@ -1051,7 +997,15 @@ void Renderer::Impl::init(int width, int height, const char* title) { pipeline_info.pMultisampleState = &multisampling; pipeline_info.pDepthStencilState = nullptr; pipeline_info.pColorBlendState = &color_blending; - pipeline_info.pDynamicState = nullptr; + const std::array dynamic_states = { + VK_DYNAMIC_STATE_VIEWPORT, + VK_DYNAMIC_STATE_SCISSOR, + }; + VkPipelineDynamicStateCreateInfo dynamic_state{}; + dynamic_state.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO; + dynamic_state.dynamicStateCount = static_cast(dynamic_states.size()); + dynamic_state.pDynamicStates = dynamic_states.data(); + pipeline_info.pDynamicState = &dynamic_state; pipeline_info.layout = pipeline_layout; pipeline_info.renderPass = render_pass; pipeline_info.subpass = 0; @@ -1232,7 +1186,139 @@ void Renderer::Impl::upload_mesh(const Mesh& mesh) { create_texture(resource, mesh.texture); } -void Renderer::Impl::draw(const Actor* const* actors, size_t actor_count) { +void Renderer::Impl::recreate_swapchain() { + int width = 0; + int height = 0; + glfwGetFramebufferSize(window, &width, &height); + if (width == 0 || height == 0) return; + check(vkDeviceWaitIdle(device), "vkDeviceWaitIdle"); + + VkSurfaceCapabilitiesKHR capabilities{}; + check(vkGetPhysicalDeviceSurfaceCapabilitiesKHR(physical_device, surface, &capabilities), + "vkGetPhysicalDeviceSurfaceCapabilitiesKHR"); + uint32_t format_count = 0; + check(vkGetPhysicalDeviceSurfaceFormatsKHR(physical_device, surface, &format_count, nullptr), + "vkGetPhysicalDeviceSurfaceFormatsKHR"); + std::vector formats(format_count); + check(vkGetPhysicalDeviceSurfaceFormatsKHR(physical_device, surface, &format_count, formats.data()), + "vkGetPhysicalDeviceSurfaceFormatsKHR"); + VkSurfaceFormatKHR surface_format = formats.front(); + for (const VkSurfaceFormatKHR& format : formats) { + if (format.format == VK_FORMAT_B8G8R8A8_SRGB && + format.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) { + surface_format = format; + break; + } + } + if (surface_format.format != swapchain_format) { + throw std::runtime_error("swapchain format changed; renderer recreation is required"); + } + uint32_t present_mode_count = 0; + check(vkGetPhysicalDeviceSurfacePresentModesKHR(physical_device, surface, &present_mode_count, + nullptr), + "vkGetPhysicalDeviceSurfacePresentModesKHR"); + std::vector present_modes(present_mode_count); + check(vkGetPhysicalDeviceSurfacePresentModesKHR(physical_device, surface, &present_mode_count, + present_modes.data()), + "vkGetPhysicalDeviceSurfacePresentModesKHR"); + VkPresentModeKHR present_mode = VK_PRESENT_MODE_FIFO_KHR; + for (VkPresentModeKHR mode : present_modes) { + if (mode == VK_PRESENT_MODE_MAILBOX_KHR) { + present_mode = mode; + break; + } + } + VkExtent2D extent{}; + if (capabilities.currentExtent.width != UINT32_MAX) { + extent = capabilities.currentExtent; + } else { + extent.width = std::clamp(static_cast(width), capabilities.minImageExtent.width, + capabilities.maxImageExtent.width); + extent.height = std::clamp(static_cast(height), capabilities.minImageExtent.height, + capabilities.maxImageExtent.height); + } + uint32_t image_count = capabilities.minImageCount + 1; + if (capabilities.maxImageCount > 0 && image_count > capabilities.maxImageCount) { + image_count = capabilities.maxImageCount; + } + + for (VkFramebuffer framebuffer : framebuffers) { + vkDestroyFramebuffer(device, framebuffer, nullptr); + } + framebuffers.clear(); + for (VkImageView view : swapchain_image_views) { + vkDestroyImageView(device, view, nullptr); + } + swapchain_image_views.clear(); + const VkSwapchainKHR old_swapchain = swapchain; + if (old_swapchain != VK_NULL_HANDLE) { + vkDestroySwapchainKHR(device, old_swapchain, nullptr); + swapchain = VK_NULL_HANDLE; + } + + VkSwapchainCreateInfoKHR create_info{}; + create_info.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR; + create_info.surface = surface; + create_info.minImageCount = image_count; + create_info.imageFormat = surface_format.format; + create_info.imageColorSpace = surface_format.colorSpace; + create_info.imageExtent = extent; + create_info.imageArrayLayers = 1; + create_info.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; + std::array families = {graphics_family, present_family}; + create_info.imageSharingMode = graphics_family == present_family + ? VK_SHARING_MODE_EXCLUSIVE + : VK_SHARING_MODE_CONCURRENT; + create_info.queueFamilyIndexCount = graphics_family == present_family ? 0 : 2; + create_info.pQueueFamilyIndices = families.data(); + create_info.preTransform = capabilities.currentTransform; + create_info.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR; + create_info.presentMode = present_mode; + create_info.clipped = VK_TRUE; + create_info.oldSwapchain = VK_NULL_HANDLE; + check(vkCreateSwapchainKHR(device, &create_info, nullptr, &swapchain), "vkCreateSwapchainKHR"); + + swapchain_extent = extent; + uint32_t actual_image_count = 0; + check(vkGetSwapchainImagesKHR(device, swapchain, &actual_image_count, nullptr), + "vkGetSwapchainImagesKHR"); + if (actual_image_count > frames.size()) { + throw std::runtime_error("resized swapchain needs more frame resources"); + } + swapchain_images.resize(actual_image_count); + check(vkGetSwapchainImagesKHR(device, swapchain, &actual_image_count, swapchain_images.data()), + "vkGetSwapchainImagesKHR"); + swapchain_image_views.resize(actual_image_count); + for (uint32_t i = 0; i < actual_image_count; ++i) { + VkImageViewCreateInfo view_info{}; + view_info.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; + view_info.image = swapchain_images[i]; + view_info.viewType = VK_IMAGE_VIEW_TYPE_2D; + view_info.format = swapchain_format; + view_info.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + view_info.subresourceRange.levelCount = 1; + view_info.subresourceRange.layerCount = 1; + check(vkCreateImageView(device, &view_info, nullptr, &swapchain_image_views[i]), + "vkCreateImageView"); + } + framebuffers.resize(actual_image_count); + for (uint32_t i = 0; i < actual_image_count; ++i) { + VkFramebufferCreateInfo framebuffer_info{}; + framebuffer_info.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO; + framebuffer_info.renderPass = render_pass; + framebuffer_info.attachmentCount = 1; + framebuffer_info.pAttachments = &swapchain_image_views[i]; + framebuffer_info.width = swapchain_extent.width; + framebuffer_info.height = swapchain_extent.height; + framebuffer_info.layers = 1; + check(vkCreateFramebuffer(device, &framebuffer_info, nullptr, &framebuffers[i]), + "vkCreateFramebuffer"); + } + images_in_flight.assign(actual_image_count, VK_NULL_HANDLE); + framebuffer_resized = false; +} + +void Renderer::Impl::draw(const Actor* const* actors, size_t actor_count, const Camera* camera) { if (!initialized || actor_count == 0) { return; } @@ -1245,6 +1331,7 @@ void Renderer::Impl::draw(const Actor* const* actors, size_t actor_count) { frame.image_available, VK_NULL_HANDLE, &image_index); if (acquire_result == VK_ERROR_OUT_OF_DATE_KHR) { + recreate_swapchain(); return; } if (acquire_result != VK_SUCCESS && acquire_result != VK_SUBOPTIMAL_KHR) { @@ -1286,6 +1373,15 @@ void Renderer::Impl::draw(const Actor* const* actors, size_t actor_count) { vkCmdBeginRenderPass(frame.command_buffer, &render_pass_begin, VK_SUBPASS_CONTENTS_INLINE); vkCmdBindPipeline(frame.command_buffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); + VkViewport viewport{}; + viewport.width = static_cast(swapchain_extent.width); + viewport.height = static_cast(swapchain_extent.height); + viewport.minDepth = 0.0F; + viewport.maxDepth = 1.0F; + VkRect2D scissor{}; + scissor.extent = swapchain_extent; + vkCmdSetViewport(frame.command_buffer, 0, 1, &viewport); + vkCmdSetScissor(frame.command_buffer, 0, 1, &scissor); for (size_t i = 0; i < actor_count; ++i) { const Actor& actor = *actors[i]; if (actor.mesh() == nullptr) continue; @@ -1301,9 +1397,26 @@ void Renderer::Impl::draw(const Actor* const* actors, size_t actor_count) { vkCmdBindDescriptorSets(frame.command_buffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline_layout, 0, 1, &resource.descriptor_set, 0, nullptr); - const auto position = actor.position(); - const auto facing = actor.facing(); - const float scale = 0.35F; + auto position = actor.global_position(); + auto facing = actor.global_facing(); + float camera_zoom = 1.0F; + if (camera != nullptr) { + const auto camera_position = camera->global_position(); + const auto camera_facing = camera->global_facing(); + const float camera_length = std::sqrt(camera_facing[0] * camera_facing[0] + + camera_facing[1] * camera_facing[1]); + const float camera_x = camera_length > 0.0001F ? camera_facing[0] / camera_length : 1.0F; + const float camera_y = camera_length > 0.0001F ? camera_facing[1] / camera_length : 0.0F; + const float dx = position[0] - camera_position[0]; + const float dy = position[1] - camera_position[1]; + position = {camera_x * dx + camera_y * dy, -camera_y * dx + camera_x * dy}; + facing = {camera_x * facing[0] + camera_y * facing[1], + -camera_y * facing[0] + camera_x * facing[1]}; + camera_zoom = camera->zoom(); + } + const float scale = 0.35F * camera_zoom; + const float horizontal_projection = + static_cast(swapchain_extent.height) / static_cast(swapchain_extent.width); float facing_x = facing[0]; float facing_y = facing[1]; const float facing_length = std::sqrt(facing_x * facing_x + facing_y * facing_y); @@ -1316,10 +1429,10 @@ void Renderer::Impl::draw(const Actor* const* actors, size_t actor_count) { } std::array constants = {{ - scale * facing_x, scale * facing_y, 0.0F, 0.0F, - -scale * facing_y, scale * facing_x, 0.0F, 0.0F, + scale * facing_x * horizontal_projection, scale * facing_y, 0.0F, 0.0F, + -scale * facing_y * horizontal_projection, scale * facing_x, 0.0F, 0.0F, 0.0F, 0.0F, 1.0F, 0.0F, - position[0], position[1], 0.0F, 1.0F, + position[0] * horizontal_projection, position[1], 0.0F, 1.0F, 0.0F, 0.0F, 0.0F, 0.0F, }}; const std::array color = actor.mesh()->color(); @@ -1354,8 +1467,10 @@ void Renderer::Impl::draw(const Actor* const* actors, size_t actor_count) { present_info.pSwapchains = &swapchain; present_info.pImageIndices = &image_index; VkResult present_result = vkQueuePresentKHR(present_queue, &present_info); - if (present_result != VK_SUCCESS && present_result != VK_SUBOPTIMAL_KHR && - present_result != VK_ERROR_OUT_OF_DATE_KHR) { + if (present_result == VK_ERROR_OUT_OF_DATE_KHR || present_result == VK_SUBOPTIMAL_KHR || + framebuffer_resized) { + recreate_swapchain(); + } else if (present_result != VK_SUCCESS) { check(present_result, "vkQueuePresentKHR"); } (void)present_result; diff --git a/src/render_loop.cpp b/src/render_loop.cpp new file mode 100644 index 0000000..f0a3021 --- /dev/null +++ b/src/render_loop.cpp @@ -0,0 +1,70 @@ +#include + +namespace boundard::render { + +RenderLoop::~RenderLoop() { + stop(); +} + +void RenderLoop::start(int width, int height, const char* title) { + if (running_) return; + + renderer_.init(width, height, title); + renderer_.set_input_callback([this](const InputEvent& event) { + input_map_.handle_event(event); + }); + running_ = true; + thread_ = std::thread(&RenderLoop::run, this); +} + +void RenderLoop::stop() { + if (!running_) return; + running_ = false; + if (thread_.joinable()) thread_.join(); + renderer_.destroy(); +} + +void RenderLoop::set_actors(std::vector actors) { + std::lock_guard lock(actors_mutex_); + actors_ = std::move(actors); +} + +void RenderLoop::set_camera(Camera* camera) { + std::lock_guard lock(actors_mutex_); + camera_ = camera; +} + +void RenderLoop::poll_events() const { + renderer_.poll_events(); +} + +bool RenderLoop::should_close() const { + return renderer_.should_close(); +} + +InputMap& RenderLoop::input() { + return input_map_; +} + +const InputMap& RenderLoop::input() const { + return input_map_; +} + +void RenderLoop::run() { + while (running_) { + std::vector actors; + Camera* camera = nullptr; + { + std::lock_guard lock(actors_mutex_); + actors = actors_; + camera = camera_; + } + if (actors.empty()) { + std::this_thread::yield(); + continue; + } + renderer_.draw(actors.data(), actors.size(), camera); + } +} + +} // namespace boundard::render diff --git a/src/scene.cpp b/src/scene.cpp index b16988b..9617b46 100644 --- a/src/scene.cpp +++ b/src/scene.cpp @@ -1,10 +1,38 @@ #include #include +#include #include namespace boundard { +namespace { + +void append_actor_tree(Actor* actor, std::vector& output) { + if (actor == nullptr) return; + output.push_back(actor); + for (const std::shared_ptr& child : actor->children()) { + append_actor_tree(child.get(), output); + } +} + +void find_cameras(Actor* actor, const std::string& name, Camera*& first_camera, + Camera*& selected_camera) { + if (actor == nullptr || selected_camera != nullptr) return; + Camera* camera = dynamic_cast(actor); + if (camera != nullptr) { + if (first_camera == nullptr) first_camera = camera; + if (!name.empty() && camera->name() == name) { + selected_camera = camera; + return; + } + } + for (const std::shared_ptr& child : actor->children()) { + find_cameras(child.get(), name, first_camera, selected_camera); + } +} + +} // namespace Scene::Scene(std::string name) : name_(std::move(name)) {} @@ -36,24 +64,44 @@ void Scene::set_actors(std::vector> actors) { void Scene::clear() { actors_.clear(); actor_storage_.clear(); + default_camera_.clear(); +} + +void Scene::set_default_camera(std::string name) { + default_camera_ = std::move(name); +} + +Camera* Scene::primary_camera() const { + Camera* first_camera = nullptr; + Camera* selected_camera = nullptr; + for (const std::shared_ptr& actor : actor_storage_) { + find_cameras(actor.get(), default_camera_, first_camera, selected_camera); + if (selected_camera != nullptr) return selected_camera; + } + return first_camera; } const std::vector& Scene::actors() const { + actors_.clear(); + for (const std::shared_ptr& actor : actor_storage_) { + append_actor_tree(actor.get(), actors_); + } return actors_; } std::vector& Scene::actors() { + static_cast(*this).actors(); return actors_; } void Scene::initialize() { - for (Actor* actor : actors_) { + for (const std::shared_ptr& actor : actor_storage_) { actor->initialize(); } } void Scene::process(float dt, const render::InputMap& input) { - for (Actor* actor : actors_) { + for (const std::shared_ptr& actor : actor_storage_) { actor->process(dt, input); } }