From 47936abed03abec0aa3efea336b27422ef0423d9 Mon Sep 17 00:00:00 2001 From: 1165565692-lang <1165565692@qq.com> Date: Tue, 9 Jun 2026 14:11:12 +0800 Subject: [PATCH] Add player camera controller --- game/docs/planning/game_architecture_board.md | 20 +++--- game/docs/planning/scene_build_plan.md | 3 +- game/src/scene/camera_controller.cpp | 72 +++++++++++++++++++ game/src/scene/camera_controller.h | 33 +++++++++ game/src/scene/whitebox_scene.cpp | 46 ++++++++---- game/src/scene/whitebox_scene.h | 8 ++- 6 files changed, 156 insertions(+), 26 deletions(-) create mode 100644 game/src/scene/camera_controller.cpp create mode 100644 game/src/scene/camera_controller.h diff --git a/game/docs/planning/game_architecture_board.md b/game/docs/planning/game_architecture_board.md index 3ff30db..2c45c49 100644 --- a/game/docs/planning/game_architecture_board.md +++ b/game/docs/planning/game_architecture_board.md @@ -148,6 +148,7 @@ mindmap 已有: - `scene/whitebox_scene.*` +- `scene/camera_controller.*`:玩家跟随、dead zone、平滑和世界边界 clamp。 下一步: @@ -408,16 +409,15 @@ mindmap 下一步建议按这个顺序做: -1. `CameraController`:修复摄像机不会跟随的问题,实现跟随、边界 clamp、dead zone 和平滑参数。 -2. `CameraZoneLock`:让 battle zone 可以临时锁定摄像机范围,清场后恢复跟随。 -3. `StageLayerSource`:让 `StageLayer` 同时支持色块占位和 sprite 资源路径。 -4. `StageAssetSlots`:建立 `stage_01` 资源槽位和素材来源记录,做到素材可直接落位。 -5. `StageProps`:把后景/前景 props 统一到 layer 数据。 -6. `BattleZone`:在现有 zone 数据上做触发、锁镜头、清场解锁。 -7. `EnemyActor`:做第一个敌人占位,接入出生点。 -8. `CombatSystem`:把 hitbox/hurtbox 真正跑起来。 -9. `SpriteAnimator`:把玩家动画从硬编码改成组件。 -10. `InputConfig`:把默认键位/手柄映射迁移到可配置数据。 +1. `CameraZoneLock`:让 battle zone 可以临时锁定摄像机范围,清场后恢复跟随。 +2. `StageLayerSource`:让 `StageLayer` 同时支持色块占位和 sprite 资源路径。 +3. `StageAssetSlots`:建立 `stage_01` 资源槽位和素材来源记录,做到素材可直接落位。 +4. `StageProps`:把后景/前景 props 统一到 layer 数据。 +5. `BattleZone`:在现有 zone 数据上做触发、锁镜头、清场解锁。 +6. `EnemyActor`:做第一个敌人占位,接入出生点。 +7. `CombatSystem`:把 hitbox/hurtbox 真正跑起来。 +8. `SpriteAnimator`:把玩家动画从硬编码改成组件。 +9. `InputConfig`:把默认键位/手柄映射迁移到可配置数据。 ## 素材协作提示 diff --git a/game/docs/planning/scene_build_plan.md b/game/docs/planning/scene_build_plan.md index feafab3..3ccc003 100644 --- a/game/docs/planning/scene_build_plan.md +++ b/game/docs/planning/scene_build_plan.md @@ -21,6 +21,7 @@ - `StageRenderer` 已支持远景、中景、地面/平台、后景 props 分层绘制,当前先用色块占位。 - `DebugOverlay` 已能显示碰撞、玩家框、battle zone、camera lock 范围和 spawn point。 - `WhiteboxScene` 已支持 `F1` 切换 debug overlay。 +- `CameraController` 已接入:修复场景 `OnUpdate()` 不被调用导致摄像机不动的问题,并提供跟随、dead zone、平滑和世界边界 clamp。 ## 2. 第一关场景目标 @@ -248,7 +249,7 @@ game/assets/stage/stage_01/ ### 场景骨架实施顺序 -1. `CameraController`:修复当前摄像机不会动的问题,实现玩家跟随、边界 clamp、dead zone 和平滑参数。 +1. 已完成:`CameraController` 修复当前摄像机不会动的问题,实现玩家跟随、边界 clamp、dead zone 和平滑参数。 2. `CameraZoneLock`:让 battle zone 可以锁定摄像机范围,先只做 debug 触发和解锁占位。 3. `StageLayerSource`:扩展 `StageLayer`,让每个 rect 支持色块占位或 sprite 资源路径。 4. `StageAssetSlots`:创建并记录 `stage_01` 资源槽位,不要求已有最终素材。 diff --git a/game/src/scene/camera_controller.cpp b/game/src/scene/camera_controller.cpp new file mode 100644 index 0000000..f0e0884 --- /dev/null +++ b/game/src/scene/camera_controller.cpp @@ -0,0 +1,72 @@ +#include "camera_controller.h" + +#include +#include + +namespace ns_game { + +namespace { + +float clampValue(float value, float minValue, float maxValue) { + if (maxValue < minValue) { + return minValue; + } + return std::clamp(value, minValue, maxValue); +} + +float smoothFactor(float smoothing, float deltaTime) { + if (smoothing <= 0.0f) { + return 1.0f; + } + return 1.0f - std::exp(-smoothing * std::max(deltaTime, 0.0f)); +} + +} // namespace + +void CameraController::Configure(const CameraControllerConfig& config) { + config_ = config; +} + +void CameraController::SnapToTarget(frostbite2D::Camera& camera, + const frostbite2D::Rect& targetBounds) { + camera.setPosition(clampToWorld( + resolveDesiredPosition(camera.getPosition(), targetBounds))); +} + +void CameraController::Update(frostbite2D::Camera& camera, + const frostbite2D::Rect& targetBounds, + float deltaTime) { + const frostbite2D::Vec2 current = camera.getPosition(); + const frostbite2D::Vec2 desired = + clampToWorld(resolveDesiredPosition(current, targetBounds)); + const float t = smoothFactor(config_.smoothing, deltaTime); + camera.setPosition(frostbite2D::Vec2( + current.x + (desired.x - current.x) * t, + current.y + (desired.y - current.y) * t)); +} + +frostbite2D::Vec2 CameraController::resolveDesiredPosition( + const frostbite2D::Vec2& currentPosition, + const frostbite2D::Rect& targetBounds) const { + frostbite2D::Vec2 desired = currentPosition; + const float targetCenterX = targetBounds.left() + targetBounds.width() * 0.5f; + const float screenX = targetCenterX - currentPosition.x; + if (screenX < config_.deadZone.left()) { + desired.x = targetCenterX - config_.deadZone.left(); + } else if (screenX > config_.deadZone.right()) { + desired.x = targetCenterX - config_.deadZone.right(); + } + desired.y = config_.worldBounds.top(); + return desired; +} + +frostbite2D::Vec2 CameraController::clampToWorld( + const frostbite2D::Vec2& position) const { + return frostbite2D::Vec2( + clampValue(position.x, config_.worldBounds.left(), + config_.worldBounds.right() - config_.viewportWidth), + clampValue(position.y, config_.worldBounds.top(), + config_.worldBounds.bottom() - config_.viewportHeight)); +} + +} // namespace ns_game diff --git a/game/src/scene/camera_controller.h b/game/src/scene/camera_controller.h new file mode 100644 index 0000000..5909b80 --- /dev/null +++ b/game/src/scene/camera_controller.h @@ -0,0 +1,33 @@ +#pragma once + +#include +#include + +namespace ns_game { + +struct CameraControllerConfig { + frostbite2D::Rect worldBounds; + float viewportWidth = 1280.0f; + float viewportHeight = 720.0f; + frostbite2D::Rect deadZone = frostbite2D::Rect(420.0f, 0.0f, 340.0f, 720.0f); + float smoothing = 14.0f; +}; + +class CameraController { +public: + void Configure(const CameraControllerConfig& config); + void SnapToTarget(frostbite2D::Camera& camera, + const frostbite2D::Rect& targetBounds); + void Update(frostbite2D::Camera& camera, const frostbite2D::Rect& targetBounds, + float deltaTime); + +private: + frostbite2D::Vec2 resolveDesiredPosition( + const frostbite2D::Vec2& currentPosition, + const frostbite2D::Rect& targetBounds) const; + frostbite2D::Vec2 clampToWorld(const frostbite2D::Vec2& position) const; + + CameraControllerConfig config_; +}; + +} // namespace ns_game diff --git a/game/src/scene/whitebox_scene.cpp b/game/src/scene/whitebox_scene.cpp index 378236a..f8ec10e 100644 --- a/game/src/scene/whitebox_scene.cpp +++ b/game/src/scene/whitebox_scene.cpp @@ -6,7 +6,6 @@ #include #include #include -#include namespace ns_game { @@ -18,11 +17,12 @@ void WhiteboxScene::onEnter() { player_->SetTopLeftPosition(level_.playerSpawn); player_->SetMovementWorld(&level_.collision); AddChild(player_); + configureCamera(); } -void WhiteboxScene::OnUpdate(float deltaTime) { - (void)deltaTime; - updateCamera(); +void WhiteboxScene::Update(float deltaTime) { + frostbite2D::Scene::Update(deltaTime); + updateCamera(deltaTime); } bool WhiteboxScene::OnEvent(const frostbite2D::Event& event) { @@ -46,6 +46,31 @@ void WhiteboxScene::Render() { debugOverlay_.Render(level_, debugFlags_, player_ ? &playerBounds : nullptr); } +frostbite2D::Camera* WhiteboxScene::activeCamera() const { + auto* renderer = frostbite2D::Application::get().getRenderer(); + if (!renderer) { + return nullptr; + } + return renderer->getCamera(); +} + +void WhiteboxScene::configureCamera() { + auto* camera = activeCamera(); + if (!camera) { + return; + } + + CameraControllerConfig config; + config.worldBounds = level_.cameraBounds; + config.viewportWidth = static_cast(config::kVirtualWidth); + config.viewportHeight = static_cast(config::kVirtualHeight); + cameraController_.Configure(config); + + if (player_) { + cameraController_.SnapToTarget(*camera, player_->GetWorldBounds()); + } +} + void WhiteboxScene::toggleDebugOverlay() { const bool enabled = !(debugFlags_.showStageGrid || debugFlags_.showCollision || debugFlags_.showActorBounds || @@ -58,22 +83,17 @@ void WhiteboxScene::toggleDebugOverlay() { debugFlags_.showSpawnPoints = enabled; } -void WhiteboxScene::updateCamera() { +void WhiteboxScene::updateCamera(float deltaTime) { if (!player_) { return; } - auto* renderer = frostbite2D::Application::get().getRenderer(); - if (!renderer || !renderer->getCamera()) { + auto* camera = activeCamera(); + if (!camera) { return; } - const auto playerPos = player_->GetTopLeftPosition(); - const float targetX = std::clamp(playerPos.x - 420.0f, - level_.cameraBounds.left(), - level_.cameraBounds.right() - - static_cast(config::kVirtualWidth)); - renderer->getCamera()->setPosition(frostbite2D::Vec2(targetX, 0.0f)); + cameraController_.Update(*camera, player_->GetWorldBounds(), deltaTime); } } // namespace ns_game diff --git a/game/src/scene/whitebox_scene.h b/game/src/scene/whitebox_scene.h index a9de451..21985e5 100644 --- a/game/src/scene/whitebox_scene.h +++ b/game/src/scene/whitebox_scene.h @@ -4,6 +4,7 @@ #include "../core/debug_flags.h" #include "../data/level_definition.h" #include "../debug/debug_overlay.h" +#include "camera_controller.h" #include "../stage/stage_renderer.h" #include @@ -14,16 +15,19 @@ namespace ns_game { class WhiteboxScene : public frostbite2D::Scene { public: void onEnter() override; - void OnUpdate(float deltaTime) override; + void Update(float deltaTime) override; bool OnEvent(const frostbite2D::Event& event) override; void Render() override; private: + frostbite2D::Camera* activeCamera() const; + void configureCamera(); void toggleDebugOverlay(); - void updateCamera(); + void updateCamera(float deltaTime); LevelDefinition level_; DebugFlags debugFlags_; + CameraController cameraController_; StageRenderer stageRenderer_; DebugOverlay debugOverlay_; frostbite2D::Ptr player_;