Add player camera controller

This commit is contained in:
2026-06-09 14:11:12 +08:00
parent 8a89346978
commit 47936abed0
6 changed files with 156 additions and 26 deletions
+10 -10
View File
@@ -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`:把默认键位/手柄映射迁移到可配置数据
## 素材协作提示
+2 -1
View File
@@ -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` 资源槽位,不要求已有最终素材。
+72
View File
@@ -0,0 +1,72 @@
#include "camera_controller.h"
#include <algorithm>
#include <cmath>
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
+33
View File
@@ -0,0 +1,33 @@
#pragma once
#include <frostbite2D/graphics/camera.h>
#include <frostbite2D/types/type_math.h>
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
+33 -13
View File
@@ -6,7 +6,6 @@
#include <frostbite2D/event/key_event.h>
#include <frostbite2D/graphics/camera.h>
#include <frostbite2D/graphics/renderer.h>
#include <algorithm>
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<float>(config::kVirtualWidth);
config.viewportHeight = static_cast<float>(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<float>(config::kVirtualWidth));
renderer->getCamera()->setPosition(frostbite2D::Vec2(targetX, 0.0f));
cameraController_.Update(*camera, player_->GetWorldBounds(), deltaTime);
}
} // namespace ns_game
+6 -2
View File
@@ -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 <frostbite2D/scene/scene.h>
@@ -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<PlayerActor> player_;